Динамические библиотеки
В Windows и OS/2 используется именно такой способ загрузки. Исполняемый
модуль в этих системах содержит ссылки на другие модули, называемые DLL
(Dynamically Loadable Library, динамически загружаемая библиотека). Фактически,
каждый модуль в этих системах обязан содержать хотя бы одну ссылку на
DLL, потому что интерфейс к системным вызовам в этих ОС также реализован
в виде DLL.
DLL представляют собой библиотеки в том смысле, что обычно они собираются
из нескольких объектных модулей. Но, в отличие от архивных библиотек,
из DLL нельзя извлечь отдельный модуль, при присоединении библиотеки к
программе она присоединяется и загружается целиком.
Главное достоинство DLL состоит в том, что модуль (как основной, так и
библиотечный), по собственному желанию, может выбирать различные библиотеки,
подгружая их уже после своей собственной загрузки. При этом нет даже строгого
ограничения на совместимость этих библиотек по вызовам (две библиотеки
совместимы по вызовам, если они имеют одинаковые точки входа с одинаковой
семантикой): загрузчик предоставляет возможность просмотреть список глобальных
символов, определенных в библиотеке и получить указатель на каждый символ,
обратившись к нему по имени' (впрочем, количество и типы параметров или
тип переменной, а тем более их семантику, загрузчик не сообщает — эту
информацию надо получать из других источников, например из списка зарегистрированных
в системе объектов СОМ).
Особенно удобна возможность вызывать любую функцию по имени при обращении
к внешним модулям из интерпретируемых языков. В примере 3.9 для подключения
внешних библиотек (в данном случае это стандартная библиотека RexxUtil
и библиотека доступа к сетевым сервисам rxSock) применяются две процедуры:
сначала RxFuncAdd с тремя параметрами: имя символа
REXX, который будет использоваться для обращения к вызываемой функции,
имя DLL и имя символа в этой DLL, а потом специальная функция, предоставляемая
модулем (sysLoadFunc и sockLoadFunc
соответственно), которая регистрирует в интерпретаторе REXX остальные
функции модуля.
Пример 3.9. Пример использования динамической библиотеки
(здесь — REXX | I Socket) в интерпретируемом языке
/**************************************************
ПРОСТОЙ HTTP клиент на REXX
Dmitry Maximovich 2:5030/544.60 aka maxim@pabl.ru
**************************************************/
PARSE VALUE ARC (1) WITH Al A2
IF Al = " THEN DO
SAY 'USAGE: wwwget hostname[/path] [port]' EXIT
END
ELSE
DO
PARSE VALUE Al WITH B1'/'B2
sServer = Bl
IF B2 = '' THEM
DO
sRequest = 'GET / HTTP 1.0'|I"ODOAODOA"x
SAY''Requesting /' END ELSE DO
sRequest = 'GET /'|IB2I|' HTTP 1.0'|I"ODOAODOA"x
SAY 'Requesting /'||B2
END
END
IF A2 <> " THEN
DO
nPortNumber = A2
END
-ELSE
DO
nPortNumber =80
END
/* Загрузить REXX Socket Library если еще не загружена*/
IF RxFuncQuery("SockLoadFuncs") THEN
DO
re = RxFuncAddf"SockLoadFuncs","rxSock","SockLoadFuncs")
re = SockLoadFuncs ()
END
IF RxFuncQuery("SysLoadFuncs") THEN
DO
re = RxFuncAdd( "SysLoadFuncs","RexxUtil","SysLoadFuncs")
re = SysLoadFuncs()
END
rc=SockGetHostByName(sServer,"host.")
IF rс <> 1 THEN
DO
SAY 'CANNOT RESOLVE HOSTNAME TO ADDRESS: 'sServer EXIT -1 END
SAY 'Trying server : 'host .name1 , address: 'host .addr' , port: 'nPortNumber
socket = SockSocket('AF_INET','SOCK_STREAM',0) IF socket < 0 THEN DO
SAY 'UNABLE TO CREATE A SOCKET' EXIT -1
END
address.familу = 'AF_INET' address.port = nPortNumber address.addr = host.addr
rc = SockConnect(socket,'address.')
IF re < 0 THEN
DO
SAY 'UNABLE TO CONNECT TO SERVER:'address.addr
SIGNAL DO
END
re = SockSend(socket, sRequest)
SAY 'REQUEST'***************************************************'
Resp = ''
DO FOREVER
re = SockRecv(socket,"sReceive",256)
IF re <= 0 THEN LEAVE
Resp = Resp || sReceive END
SAY
/* CR -> CRLF */
nStart = 1
nStop = pos(X2C("OA"), Resp)
do while nStop > 0
SAY SUBSTR(Resp,nStart,nStop-nStart)
nStart = nStop + 1
nStop = pos(X2C("OA"), Resp, nStart)
end
DO:
rc = SockShutDown(socket,2)
rc = SockClose(socket)
При сборке DLL из нескольких объектных модулей программист должен предоставить
DEF-файл (пример 3.10). В этом файле содержится перечисление символов,
экспортируемых библиотекой (в отличие от обычных, "архивных"
библиотек, набор этих символов не обязательно равен объединению наборов
экспортных символов всех включенных в библиотеку объектов), а также некоторые
другие параметры. Например, можно указать, что DLL имеет функции инициализации
и терминации. Эти функции могут запускаться как при первой загрузке библиотеки
(INITGLOBAL), так и при подключении библиотеки
очередной программой (INITINSTANCE). Можно также
управлять разделением сегмента данных DLL — применять общий сегмент данных
для всех программ, использующих библиотеку, или создавать свою копию для
каждой программы.
Пример 3.10. DEF-файл из примеров кода VisualAge
C++ V3.0
LIBRARY REXXUTIL INITINSTANCE LONGNAMES
PROTMODE
DESCRIPTION 'REXXUTIL Utilities - (c) Copyright IBM Corporation 1991'
DATA MULTIPLE NONSHARED STACKSIZE 32768
EXPORTS
SYSCLS = SysCls @1 SYSCURPOS = SysCurPos @2 SYSCURSTATE = SysCurState
@3 SYSDRIVEINFO = SysDrivelnfo @4
SYSDRIVEMAP = SysDriveMap @5
SYSDROPF0NCS = SysDropFuncs @6
SYSFILEDELETE = SysFileDelete @7
SYSFILESEARCH = SysFileSearch @8
SYSFILETREE - SysFileTree @9
SYSGETMESSAGE = SysGetMessage 010
SYSINI = Syslni 011
SYSLOADFUNCS = SysLoadFuncs @12
SYSMKDIR = SysMkDir @13
SYSOS2VER = SysOS2Ver 014
SYSRMDIR = SysRmDir @15
SYSSEARCHPATH = SysSearchPath @16
SYSSLEEP = SysSleep @17
SYSTEMPFILENAME = SysTempFileName @18
SYSTEXTSCREENREAD = SysTextScreenRead @19
SYSTEXTSCREENSIZE = SysTextScreenSize La20
SYSGETEA = SysGetEA @21
SYSPUTEA = SysPutEA @22
SYSWAITNAMEDPIPE = SysWaitNamedPipe @23
DLL являются удобным средством разделения кода и создания отдельно загружаемых
программных модулей, но их использование сопряжено с определенной проблемой,
которая будет подробнее объясняться в разд.
Разделяемые библиотеки. Забегая вперед, скажем, что концепция разделяемых
DLL наиболее естественна в системах, где веб задачи используют единое
адресное пространство — но при этом ошибка в любой из программ может привести
к порче данных или кода другой задачи. Стандартный же способ борьбы с
этой проблемой — выделение каждому процессу своего адресного пространства
— значительно усложняет разделение кода.
Другая проблема, обусловленная широким использованием разделяемого кода,
состоит в слежении за версией этого кода. Действительно, представим себе
жизненную ситуацию: в системе одновременно загружены тридцать программ,
использующие библиотеку LIBC.DLL. При этом десять из них разрабатывались
и тестировались с версией 1.0 этой библиотеки, пять — с версией 1.5 и
пятнадцать — с версией 1.5а. Понятно, что рассчитывать на устойчивую работу
всех тридцати программ можно только при условии, что все три версии библиотеки
полностью совместимы снизу вверх не только по набору вызовов и их параметров,
но и по точной семантике каждого из этих вызовов. Последнее требование
иногда формулируют как bug-for-bug compatibility
(корректно перевести это словосочетание можно так: полная
совместимость не только по спецификациям, но и по отклонениям от них).
Казалось бы, исправление ошибок должно лишь улучшать работу программ,
использующих исправленный код. На практике же бывают ситуации, когда код
основной программы содержит собственные обходные пути, компенсирующие
ошибки в библиотеке. Эти обходы могут быть как внесены сознательно (когда
поставщик библиотеки исправит, еще неизвестно, а программа нужна сейчас),
так и получиться сами собой (арифметический знак, перепутанный четное
число раз и т. д.). В этих случаях исправление ошибки может привести к
труднопредсказуемым последствиям. Нельзя также забывать и о возможности
внесения новых ошибок при исправлении старых, поэтому при разработке и
эксплуатации сложных программных систем, необходимо тщательно следить
за тем, что именно и где изменилось, а не просто фиксировать ошибки.
Требование "совместимости с точностью до ошибок" — это лишь
полемически заостренная формулировка требования контролируемости поведения
кода. Из вышеприведенных соображений понятно, что нарушения такой контролируемости
представляют собой проблему, которая, не будучи так или иначе разрешена,
может серьезно усложнить работу администраторов системы и приложений.
Разделяемый код в системах семейства
Windows
Катастрофические масштабы эта проблема принимает в системах семейства
Windows, где принято помещать в дистрибутивы прикладных программ все потенциально
разделяемые модули, которые этой программе могут потребоваться — среда
исполнения компилятора и т. д. При этом каждое приложение считает своим
долгом поместить свои разделяемые модули в C:\WINDOWS\SYSTEM32 (в Windows
NT/2000/XP это заодно приводит к тому, что установка самой безобидной
утилиты требует администраторских привилегий). Средств же проследить за
тем, кто, какую версию, чего, куда и зачем положил, практически не предоставляется.
В лучшем случае установочная программа спрашивает: "Тут вот у вас
что-то уже лежит, перезаписать?". Стандартный деинсталлятор содержит
список DLL, которые принадлежат данному приложению, и осознает тот факт,
что эти же DLL используются кем-то еще, но не предоставляет (и, по-видимому,
не пытается собрать) информации о том, кем именно они используются. Наличие
реестра объектов СОМ не решает проблемы, потому что большая часть приносимого
каждым приложением "разделяемого" кода (кавычки стоят потому,
что значительная часть этого кода никому другому, кроме принесшего его
приложения, не нужна) не является сервером СОМ.
В результате, когда, например, после установки MS Project 2000 перестает
работать MS Office 2000 [MSkb RU270125], это никого не удивляет, а конфликты
между приложениями различных разработчиков или разных "поколений"
считаются неизбежными. Установить же в одной системе и использовать хотя
бы попеременно две различные версии одного продукта просто невозможно
— однако, когда каждая версия продукта использует собственный формат данных,
а конверсия между ними неидеальна, это часто оказывается желательно.
Разработчики же и тестеры, которым надо обеспечить совместимость с различными
версиями существующих приложений, при этом просто оказываются в безвыходной
ситуации. Неслучайно поставщики VmWare (системы виртуальных машин для
х8б) как одно из главных достоинств своей системы рекламируют возможность
держать несколько копий Windows одновременно загруженными на одной машине.
Привлекательный путь решения этой проблемы — давать каждому приложе-нию
возможность указывать, какие именно DLL ему нужны и где их искать, и позволять
одновременно загружать одноименные DLL с разной семантикой — на самом
деле вовсе не прост как с точки зрения реализации, так и с точки зрения
управления системой. Системы с виртуальной памятью предлагают некоторые
подходы к реализации этого пути, но это будет обсуждаться в разд. Разделяемые
библиотеки.
|