Вы находитесь на страницах старой версии сайта.
Переходите на новую версию Interface.Ru

предыдущая статья серии

Программирование на языке Delphi. Глава 5. Динамически загружаемые библиотеки. Часть 2

© А.Н. Вальвачев, К.А. Сурков, Д.А. Сурков, Ю.М. Четырько
Статья была опубликована на сайте rsdn.ru

Использование библиотеки в программе

Для того чтобы в прикладной программе воспользоваться процедурами и функциями библиотеки, необходимо выполнить так называемый импорт. Импорт обеспечивает загрузку библиотеки в оперативную память и привязку записанных в программе команд вызова к адресам соответствующих процедур и функций библиотеки. Существуют два способа импорта, отличающихся по удобству и гибкости программирования:

Статический импорт является более удобным, а динамический — более гибким.

Статический импорт

При статическом импорте все действия по загрузке и подключению библиотеки выполняются автоматически операционной системой во время запуска главной программы. Чтобы задействовать статический импорт, достаточно просто объявить в программе процедуры и функции библиотеки как внешние. Это делается с помощью директивы external, например:

procedure BubleSortIntegers(var Arr: array of Integer); stdcall;
  external 'SortLib.dll';

procedure QuickSortIntegers(var Arr: array of Integer); stdcall;
  external 'SortLib.dll';

После ключевого слова external записывается имя двоичного файла библиотеки в виде константной строки или константного строкового выражения. Вместе с директивой external может использоваться уже известная вам директива name, которая служит для явного указания экспортного имени процедуры в библиотеке. С ее помощью объявления процедур можно переписать по-другому:

procedure BubleSort(var Arr: array of Integer); stdcall;
  external 'SortLib.dll' name 'BubleSortIntegers';

procedure QuickSort(var Arr: array of Integer); stdcall;
  external 'SortLib.dll' name 'QuickSortIntegers';

Поместив в программу приведенные выше объявления, можно вызывать процедуры BubleSort и QuickSort, как будто они являются частью самой программы. Давайте это проверим.

Шаг 6. Создайте новую консольную программу. Для этого выберите в меню команду File | New | Other... и в открывшемся диалоговом окне выделите значок Console Application. Затем нажмите кнопку OK.

Шаг 7. Добавьте в программу external-объявления процедур BubleSort и QuickSort, а также наберите приведенный ниже текст программы. Сохраните проект под именем TestStaticImport.dpr.

program TestStaticImport;

{$APPTYPE CONSOLE}

procedure BubleSort(var Arr: array of Integer); stdcall;
  external 'SortLib.dll' name 'BubleSortIntegers';
procedure QuickSort(var Arr: array of Integer); stdcall;
  external 'SortLib.dll' name 'QuickSortIntegers';

var
  Arr: array [0..9] of Integer;
  I: Integer;

begin
  // Метод «пузырька»
  Randomize;
  for I := Low(Arr) to High(Arr) do
    Arr[I] := Random(100); // Заполнение массива случайными числами
  BubleSort(Arr);
  for I := Low(Arr) to High(Arr) do
    Write(Arr[I], ' ');
  Writeln;
  // Метод быстрой сортировки
  for I := Low(Arr) to High(Arr) do
    Arr[I] := Random(100); // Заполнение массива случайными числами
  QuickSort(Arr);
  for I := Low(Arr) to High(Arr) do
    Write(Arr[I], ' ');
  Writeln;
  Writeln('Press Enter to exit...');
  Readln;
end.

Шаг 8. Выполните компиляцию и запустите программу. Если числа печатаются на экране по возрастанию, то сортировка работает правильно.

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

Модуль импорта

При разработке динамически загружаемых библиотек нужно всегда думать об их удобном использовании. Давайте, например, обратимся к последнему примеру и представим, что в библиотеке не две процедуры, а сотня, и нужны они не в одной программе, а в нескольких. В этом случае намного удобнее вынести external-объявления процедур в отдельный модуль, подключаемый ко всем программам в секции uses. Такой модуль условно называют модулем импорта. Кроме объявлений внешних подпрограмм он обычно содержит определения типов данных и констант, которыми эти подпрограммы оперируют.

Модуль импорта для библиотеки SortLib будет выглядеть так:

unit SortLib;

interface

procedure BubleSort(var Arr: array of Integer); stdcall;
procedure QuickSort(var Arr: array of Integer); stdcall;

implementation

const
  DllName = 'SortLib.dll';

procedure BubleSort(var Arr: array of Integer); external
  DllName name 'BubleSortIntegers';
procedure QuickSort(var Arr: array of Integer); external
  DllName name 'QuickSortIntegers';

end.

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

Динамический импорт

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

Для динамического импорта необходимо загрузить библиотеку в оперативную память вызовом функции LoadLibrary, а затем извлечь из нее адреса подпрограмм с помощью функции GetProcAddress. Полученные адреса нужно сохранить в процедурных переменных соответствующего типа. После этого вызов подпрограмм библиотеки может выполняться путем обращения к процедурным переменным. Для завершения работы с библиотекой необходимо вызвать функцию FreeLibrary.

Ниже приведено краткое описание функций LoadLibrary, FreeLibrary и GetProcAddress.

Приведенная ниже программа TestDynamicImport аналогична по функциональности программе TestStaticImport, но вместо статического импорта использует технику динамического импорта:

program TestDynamicImport;

{$APPTYPE CONSOLE}

uses
  Windows;

type
  TBubleSortProc = procedure (var Arr: array of Integer); stdcall;
  TQuickSortProc = procedure (var Arr: array of Integer); stdcall;

var
  BubleSort: TBubleSortProc; // указатель на функцию BubleSort
  QuickSort: TQuickSortProc; // указатель на функцию QuickSort
  LibHandle: HModule;        // описатель библиотеки

  Arr: array [0..9] of Integer;
  I: Integer;

begin
  LibHandle := LoadLibrary('SortLib.dll');
  if LibHandle <> 0 then
  begin
    @BubleSort := GetProcAddress(LibHandle, 'BubleSortIntegers');
    @QuickSort := GetProcAddress(LibHandle, 'QuickSortIntegers');
    if (@BubleSort <> nil) and (@QuickSort <> nil) then
    begin
      Randomize;
      for I := Low(Arr) to High(Arr) do
        Arr[I] := Random(100);
      BubleSort(Arr);
      for I := Low(Arr) to High(Arr) do
        Write(Arr[I], ' ');
      Writeln;
      for I := Low(Arr) to High(Arr) do
        Arr[I] := Random(100);
      QuickSort(Arr);
      for I := Low(Arr) to High(Arr) do
        Write(Arr[I], ' ');
      Writeln;
    end
    else
      Writeln('Ошибка отсутствия процедуры в библиотеке.');
    FreeLibrary(LibHandle);
  end
  else
    Writeln('Ошибка загрузки библиотеки.');
  Writeln('Press Enter to exit...');
  Readln;
end.

В программе определены два процедурных типа данных, которые по списку параметров и правилу вызова (stdcall) соответствуют подпрограммам сортировки BubleSort и QuickSort в библиотеке:

type
  TBubleSortProc = procedure (var Arr: array of Integer); stdcall;
  TQuickSortProc = procedure (var Arr: array of Integer); stdcall;

Эти типы данных нужны для объявления процедурных переменных, в которых сохраняются адреса подпрограмм:

var
  BubleSort: TBubleSortProc; 
  QuickSort: TQuickSortProc;

В секции var объявлена также переменная для хранения целочисленного описателя библиотеки, возвращаемого функцией LoadLibrary:

var
  ... 
  LibHandle: HModule;

Программа начинает свою работу с того, что вызывает функцию LoadLibrary, в которую передает имя файла DLL-библиотеки. Функция возвращает описатель библиотеки, который сохраняется в переменной LibHandle.

LibHandle := LoadLibrary('SortLib.dll');
  if LibHandle <> 0 then
  begin
    ...
  end

Если значение описателя отлично от нуля, значит библиотека была найдена на диске и успешно загружена в оперативную память. Убедившись в этом, программа обращается к функции GetProcAddress за адресами подпрограмм. Полученные адреса сохраняются в соответствующих процедурных переменных:

    @BubleSort := GetProcAddress(LibHandle, 'BubleSortIntegers');
    @QuickSort := GetProcAddress(LibHandle, 'QuickSortIntegers');

Обратите внимание на использование символа @ перед именем каждой переменной. Он говорит о том, что выполняется не вызов подпрограммы, а работа с ее адресом.

Если этот адрес отличен от значения nil, значит подпрограмма с указанным именем была найдена в библиотеке и ее можно вызвать путем обращения к процедурной переменной:

    if (@BubleSort <> nil) and (@QuickSort <> nil) then
    begin
      ...
      BubleSort(Arr);
      ...
      QuickSort(Arr);
      ...
    end

По окончании сортировки программа выгружает библиотеку вызовом функции FreeLibrary.

Как вы убедились, динамический импорт в сравнении со статическим требует значительно больше усилий на программирование, но он имеет ряд преимуществ:

Динамический импорт отлично подходит для работы с библиотеками драйверов устройств. Он, например, используется самой средой Delphi для работы с драйверами баз данных.

Использование библиотеки из программы на языке C++

Созданные в среде Delphi библиотеки можно использовать в других языках программирования, например в языке C++. Язык C++ получил широкое распространение как язык системного программирования, и в ряде случаев программистам приходится прибегать к нему.

Ниже показано, как выполнить импорт подпрограмм BubleSort и QuickSort в языке C++.

extern "C" __declspec(dllimport) 
void __stdcall BubleSort(int* Array, int HighIndex);

extern "C" __declspec(dllimport) 
void __stdcall QuickSort(int* Array, int HighIndex);

Не углубляясь в детали синтаксиса, заметим, что в языке C++ отсутствуют открытые массивы в параметрах подпрограмм. Тем не менее, программист может вызывать такие подпрограммы, основываясь на том, что открытый массив неявно состоит из двух параметров: указателя на начало массива и номера последнего элемента.

Глобальные переменные и константы

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

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

Инициализация и завершение работы библиотеки

Инициализация библиотеки происходит при ее подключении к программе и состоит в выполнении секций initialization во всех составляющих библиотеку модулях, а также в ее главном программном блоке. Завершение работы библиотеки происходит при отключении библиотеки от программы; в этот момент в каждом модуле выполняется секция finalization. Используйте эту возможность тогда, когда библиотека запрашивает и освобождает какие-то системные ресурсы, например файлы или соединения с базой данных. Запрос ресурса выполняется в секции initialization, а его освобождение — в секции finalization.

Существует еще один способ инициализации и завершения библиотеки, основанный на использовании предопределенной переменной DllProc. Переменная DllProc хранит адрес процедуры, которая автоматически вызывается при отключении библиотеки от программы, а также при создании и уничтожении параллельных потоков в программах, использующих DLL-библиотеку (потоки обсуждаются в главе 14). Ниже приведен пример использования переменной DllProc:

library MyLib;

var
  SaveDllProc: TDLLProc;

procedure LibExit(Reason: Integer);
begin
  if Reason = DLL_PROCESS_DETACH then
  begin
    ...                    // завершение библиотеки
  end;
  SaveDllProc(Reason);     // вызов предыдущей процедуры
end;

begin
  ...                      // инициализация библиотеки
  SaveDllProc := DllProc;  // сохранение предыдущей процедуры
  DllProc := @LibExit;     // установка процедуры LibExit
end.

Процедура LibExit получает один целочисленный аргумент, который уточняет причину вызова. Возможные значения аргумента:

Обратите внимание, что установка значения переменной DllProc выполняется в главном программном блоке, причем предыдущее значение сохраняется для вызова "по цепочке".

Мы рекомендуем вам прибегать к переменной DllProc лишь в том случае, если библиотека должна реагировать на создание и уничтожение параллельных потоков. Во всех остальных случаях лучше выполнять инициализацию и завершение с помощью секций initialization и finalization.

Исключительные ситуации и ошибки выполнения подпрограмм

Для поддержки исключительных ситуаций среда Delphi использует средства операционной системы Window. Поэтому, если в библиотеке возникает исключительная ситуация, которая никак не обрабатывается, то она передается вызывающей программе. Программа может обработать эту исключительную ситуацию самым обычным способом — с помощью операторов try … except ... end. Такие правила действуют для программ и DLL-библиотек, созданных в среде Delphi. Если же программа написана на другом языке программирования, то она должна обрабатывать исключение в библиотеке, написанной на языке Delphi как исключение операционной системы с кодом $0EEDFACE. Адрес инструкции, вызвавшей исключение, содержится в первом элементе, а объект, описывающий исключение, — во втором элементе массива ExceptionInformation, который является частью системной записи об исключительной ситуации.

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

Общий менеджер памяти

Если выделение и освобождение динамической памяти явно или неявно поделены между библиотекой и программой, то и в библиотеке, и в программе следует обязательно подключить модуль ShareMem. Его нужно указать в секции uses первым, причем как в библиотеке, так и в использующей ее программе.

Модуль ShareMem является модулем импорта динамически загружаемой библиотеки Borlndmm.dll, которая должна распространяться вместе с вашей программой. В момент инициализации модуль ShareMem выполняет подмену стандартного менеджера памяти на менеджер памяти из библиотеки Borlndmm.dll. Благодаря этому библиотека и программа могут выделять и освобождать память совместно.

Модуль ShareMem следует подключать еще и в том случае, если между библиотекой и программой происходит передача длинных строк или динамических массивов. Поскольку длинные строки и динамические массивы размещаются в динамической памяти и управляются автоматически (путем подсчета количества ссылок), то блоки памяти для них, выделяемые программой, могут освобождаться библиотекой (а также наоборот). Использование единого менеджера памяти из библиотеки Borlndmm.dll избавляет программу и библиотеку от скрытых разрушений памяти.


ПРИМЕЧАНИЕ

Последнее правило не относится к отрытым массивам-параметрам, которые мы использовали в подпрограммах BubleSort и QuickSort при создании библиотеки SortLib.dll.


Стандартные системные переменные

Как вы уже знаете, в языке Delphi существует стандартный модуль System, неявно подключаемый к каждой программе или библиотеке. В этом модуле содержатся предопределенные системные подпрограммы и переменные. Среди них имеется переменная IsLibrary с типом Boolean, значение которой равно True для библиотеки и False для обычной программы. Проверив значение переменной IsLibrary, подпрограмма может определить, является ли она частью библиотеки.

В модуле System объявлена также переменная CmdLine: PChar, содержащая командную строку, которой была запущена программа. Библиотеки не могут запускаться самостоятельно, поэтому для них переменная CmdLine всегда содержит значение nil.

Итоги

Прочитав главу, вы наверняка вздохнули с облегчением. Жизнь стала легче: сделал одну уникальную по возможностям библиотеку и вставляй ее во все программы! Нужно подключить к Delphi-программе модуль из другой среды программирования — пожалуйста! И все это делается с помощью динамически загружаемых библиотек. Надеемся, вы освоили технику работы с ними и осилите подключение к своей программме библиотек, написанных не только на языке Delphi, но и на языках C и C++. В следующей главе мы рассмотрим некоторые другие взаимоотношения между программами, включая управление объектами одной программы из другой.

Дополнительная информация

За дополнительной информацией обращайтесь в компанию Interface Ltd.

Обсудить на форуме Borland

Рекомендовать страницу

INTERFACE Ltd.
Телефон/Факс: +7 (495) 925-0049
Отправить E-Mail
http://www.interface.ru
Rambler's Top100
Ваши замечания и предложения отправляйте редактору
По техническим вопросам обращайтесь к вебмастеру
Дата публикации: 30.03.06