Королевство Дельфи"Knowledge itself is power"
F.Bacon
 Лицей
  
Главная
О лицее

Список семинаров

 
 К н и г и
 
Книжная полка
 
 
Библиотека
 
  
  
 


Поиск
 
Поиск по КС
Поиск в статьях
Яndex© + Google©
Поиск книг

 
  
Тематический каталог
Все манускрипты

 
  
Карта VCL
ОШИБКИ
Сообщения системы

 
Форумы
 
Круглый стол
Новые вопросы

 
  
Базарная площадь
Городская площадь

 
   
С Л С

 
Летопись
 
Королевские Хроники
Рыцарский Зал
Глас народа!

 
  
ТТХ
Конкурсы
Королевская клюква

 
Разделы
 
Hello, World!
Лицей

Квинтана

 
  
Сокровищница
Подземелье Магов
Подводные камни
Свитки

 
  
Школа ОБЕРОНА

 
  
Арсенальная башня
Фолианты
Полигон

 
  
Книга Песка
Дальние земли

 
  
АРХИВЫ

 
 

Сейчас на сайте присутствуют:
 
  
 
Во Флориде и в Королевстве сейчас  11:40[Войти] | [Зарегистрироваться]

Глава 5. Динамически загружаемые библиотеки

Кирилл Сурков
Александр Вальвачев, Дмитрий Сурков, Юрий Четырько
дата публикации 22-02-2006 11:03

урок из цикла: Учебное пособие по программированию на языке Delphi


предыдущий урок содержание семинара следующий урок

Глава 5. Динамически загружаемые библиотеки

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

5.1. Динамически загружаемые библиотеки

Динамически загружаемая библиотека (от англ. dynamically loadable library) — это библиотека подпрограмм, которая загружается в оперативную память и подключается к использующей программе во время ее работы (а не во время компиляции и сборки). Файлы динамически загружаемых библиотек в среде Windows обычно имеют расширение .dll (от англ. Dynamic-Link Library). Для краткости в этой главе мы будем использовать термин динамическая библиотека, или даже просто библиотека, подразумевая DLL-библиотеку.

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

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

5.2. Разработка библиотеки

5.2.1. Структура библиотеки

По структуре исходный текст библиотеки похож на исходный текст программы, за исключением того, что текст библиотека начинается с ключевого слова library, а не слова program. Например:

library SortLib;

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

Если в теле библиотеки объявлены некоторые процедуры,

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

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

exports
  BubleSort,
  QuickSort;

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

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

library SortLib;

procedure BubleSort(var Arr: array of Integer); 
var
  I, J, T: Integer;
begin
  for I := Low(Arr) to High(Arr) - 1 do
    for J := I + 1 to High(Arr) do
      if Arr[I] > Arr[J] then
      begin
        T := Arr[I];
        Arr[I] := Arr[J];
        Arr[J] := T;
      end;
end;

exports
  BubleSort;

begin
end.

Исходный текст динамически загружаемой библиотеки заканчивается операторным блоком begin...end, в который можно вставить любые операторы для подготовки библиотеки к работе. Эти операторы выполняются во время загрузки библиотеки основной программой. Наша простейшая библиотека SortLib не требует никакой подготовки к работе, поэтому ее операторный блок пустой.

5.2.2. Экспорт подпрограмм

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

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

exports
  BubleSort;

то это означает, что экспортное имя процедуры будет 'BubleSort'. При желании это имя можно сделать отличным от программного имени, дополнив описание директивой name, например:

exports
  BubleSort name 'BubleSortIntegers';

В итоге, экспортное имя процедуры BubleSort будет 'BubleSortIntegers'.

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

procedure QuickSort(var Arr: array of Integer); overload; // для целых чисел
procedure QuickSort(var Arr: array of Real); overload;    // для вещественных

то при экспорте этим двум процедурам необходимо явно указать отличные друг от друга экспортные имена:

exports
  QuickSort(var Arr: array of Integer) name 'QuickSortIntegers';
  QuickSort(var Arr: array of Real) name 'QuickSortReals';

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

5.2.3. Соглашения о вызове подпрограмм

В главе 2 мы уже кратко рассказывали о том, что в различных языках программирования используются различные правила вызова подпрограмм, и что для совместимости с ними в языке Delphi существуют директивы register, stdcall, pascal и cdecl. Применение этих директив становится особенно актуальным при разработке динамически загружаемых библиотек, которые используются в программах, написанных на других языках программирования.

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

Стек — это область памяти, в которую данные помещаются в прямом порядке, а и извлекаются в обратном, по аналогии с наполнением и опустошением магазина патронов у стрелкового оружия. Очередность работы с элементами в стеке обозначается термином LIFO (от англ. Last In, First Out — последним вошел, первым вышел).

Примечание: Существует еще обычная очередность работы с элементами, обозначаемая термином FIFO (от англ. First In, First Out — первым вошел, первым вышел).

Для каждой программы на время работы создается свой стек. Через него передаются параметры подпрограмм и в нем же сохраняются адреса возврата из этих подпрограмм. Именно благодаря стеку подпрограммы могут вызывать друг друга, или даже рекурсивно сами себя.

Вызов подпрограммы состоит из "заталкивания" в стек всех аргументов и адреса следующей команды (для воврата к ней), а затем передачи управления на начало подпрограммы. По окончании работы подпрограммы из стека извлекается адрес воврата с передачей управления на этот адрес; одновременно с этим из стека выталкиваются аргументы. Происходит так называемая очистка стека. Это общая схема работы и у нее бывают разные реализации. В частности, аргументы могут помещаться в стек либо в прямом порядке (слева направо, как они перечислены в описании подпрограммы), либо в обратном порядке (справа налево), либо вообще, не через стек, а через свободные регистры процессора для повышения скорости работы. Кроме того, очистку стека может выполнять либо вызываемая подпрограмма, либо вызывающая программа. Выбор конкретного соглашения о вызове обеспечивают директивы register, pascal, cdecl и stdcall. Их смысл поясняет таблица 5.1.

ДирективаПорядок занесения аргументов в стекКто отвечает за очистку стекаПередача аргументов через регистры
registerСлева направоПодпрограммаДа
pascalСлева направоПодпрограммаНет
cdeclСправа налевоВызывающая программаНет
stdcallСправа налевоПодпрограммаНет

Таблица 5.1. Соглашения о вызове подпрограмм

Примечание: Директива register не означает, что все аргументы обязательно передаются через регистры процессора. Если число аргументов больше числа свободных регистров, то часть аргументов передается через стек.

Возникает резонный вопрос: какое соглашение о вызове следует выбирать для процедур и функций динамически загружаемых библиотек. Ответ — соглашение stdcall:

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

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

5.2.4. Пример библиотеки

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

Шаг 1. Запустите систему Delphi и выберите в меню команду File | New | Other... . В диалоговом окне, которое откроется на экране, выберите значок с подписью DLL Wizard и нажмите кнопку OK (рисунок 5.1):


Рисунок 5.1. Окно выбора нового проекта, в котором выделен пункт DLL Wizard

Среда Delphi создаст новый проект со следующей заготовкой библиотеки:

library Project1;

{ Important note about DLL memory management ... }

uses
  SysUtils,
  Classes;

begin
end.

Шаг 2. С помощью команды File | New | Unit создайте в проекте новый программный модуль. Его заготовка будет выглядеть следующим образом:

unit Unit1;

interface

implementation

end.

Шаг 3. Сохраните модуль под именем SortUtils.pas, а проект — под именем SortLib.dpr. Прейдите к главному файлу проекта и удалите из секции uses модули SysUtils и Classes (они сейчас не нужны). Главный программный модуль должен стать следующим:

library SortLib;

{ Important note about DLL memory management ... }

uses
  SortUtils in 'SortUtils.pas';

begin
end.

Шаг 4. Наберите исходный текст модуля SortUtils:

unit SortUtils;

interface

procedure BubleSort(var Arr: array of Integer); stdcall;
procedure QuickSort(var Arr: array of Integer); stdcall; 
 
exports
  BubleSort name 'BubleSortIntegers',
  QuickSort name 'QuickSortIntegers';

implementation

procedure BubleSort(var Arr: array of Integer);
var
  I, J, T: Integer;
begin
  for I := Low(Arr) to High(Arr) - 1 do
    for J := I + 1 to High(Arr) do
      if Arr[I] > Arr[J] then
      begin
        T := Arr[I];
        Arr[I] := Arr[J];
        Arr[J] := T;
      end;
end;

procedure QuickSortRange(var Arr: array of Integer; Low, High: Integer);
var
  L, H, M: Integer;
  T: Integer;
begin
  L := Low;
  H := High;
  M := (L + H) div 2;
  repeat
    while Arr[L] < Arr[M] do
      L := L + 1;
    while Arr[H] > Arr[M] do
      H := H - 1;
    if L <= H then
    begin
      T := Arr[L];
      Arr[L] := Arr[H];
      Arr[H] := T;
      if M = L then
        M := H
      else if M = H then
        M := L;
      L := L + 1;
      H := H - 1;
    end;
  until L > H;
  if H > Low then QuickSortRange(Arr, Low, H);
  if L < High then QuickSortRange(Arr, L, High);
end;

procedure QuickSort(var Arr: array of Integer);
begin
  if Length(Arr) > 1 then
    QuickSortRange(Arr, Low(Arr), High(Arr));
end;

end.

В этом модуле процедуры BubleSort и QuickSort сортируют массив чисел двумя способами: методом "пузырька" и методом "быстрой" сортировки соответственно. С их реализацией мы предоставляем вам разобраться самостоятельно, а нас сейчас интересует правильное оформление процедур для их экспорта из библиотеки.

Директива stdcall, использованная при объявлении процедур BubleSort и QuickSort,

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

позволяет вызывать процедуры не только из программ на языке Delphi, но и из программ на языках C/C++ (далее мы покажем, как это сделать).

Благодаря присутствию в модуле секции exports,

exports
  BubleSort name 'BubleSortIntegers',
  QuickSort name 'QuickSortIntegers';

подключение модуля в главном файле библиотеки автоматически приводит к экспорту процедур.

Шаг 5. Сохраните все файлы проекта и выполните компиляцию. В результате вы получите на диске в своем рабочем каталоге двоичный файл библиотеки SortLib.dll. Соответствующее расширение назначается файлу автоматически, но если вы желаете, чтобы компилятор назначал другое расширение, воспользуйтесь командой меню Project | Options… и в появившемся окне Project Options на вкладке Application впишите расширение файла в поле Target file extension (рисунок 5.2).


Рисунок 5.2. Окно настройки параметров проекта

Кстати, с помощью полей LIB Prefix, LIB Suffix и LIB Version этого окна вы можете задать правило формирования имени файла, который получается при сборке библиотеки. Имя файла составляется по формуле:

<LIB Prefix> + <имя проекта> + <LIB Suffix> + '.' + <Target file extention> + [ '.' + <LIB Version> ]

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

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

  • статический импорт (обеспечивается директивой компилятора external);
  • динамический импорт (обеспечивается функциями LoadLibrary и GetProcAddress).

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

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

При статическом импорте все действия по загрузке и подключению библиотеки выполняются автоматически операционной системой во время запуска главной программы. Чтобы задействовать статический импорт, достаточно просто объявить в программе процедуры и функции библиотеки как внешние. Это делается с помощью директивы 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. Выполните компиляцию и запустите программу. Если числа печатаются на экране по возрастанию, то сортировка работает правильно.

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

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

При разработке динамически загружаемых библиотек нужно всегда думать об их удобном использовании. Давайте, например, обратимся к последнему примеру и представим, что в библиотеке не две процедуры, а сотня, и нужны они не в одной программе, а в нескольких. В этом случае намного удобнее вынести 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.

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

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

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

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

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

LoadLibrary(LibFileName: PChar): HModule — загружает в оперативную память библиотеку, которая хранится на диске в файле с именем LibFileName. При успешном выполнении функция возвращает числовой описатель библиотеки, который должен использоваться в дальнейшем для управления библиотекой. Если при загрузке библиотеки призошла какая-нибудь ошибка, то возвращается нулевое значение. Если аргумент LibFileName содержит имя файла без маршрута, то этот файл ищется в следущих каталогах: в каталоге, из которого была запущена главная программа, в текущем каталоге, в системном каталоге операционной системы Windows (его точный маршрут можно узнать вызовом функции GetSystemDirectory), в каталоге, по которому установлена операционная система (его точный маршрут можно узнать вызовом функции GetWindowsDirectory), а также в каталогах, перечисленных в переменной окружения PATH.

FreeLibrary(LibModule: HModule): Bool — выгружает библиотеку, заданную описателем LibModule, из оперативной памяти и освобождает занимаемые библиотекой ресурсы системы.

GetProcAddress(Module: HModule; ProcName: PChar): Pointer — возвращает адрес подпрограммы с именем ProcName в библиотеке с описателем Module. Если подпрограмма с именем ProcName в библиотеке не существует, то функция возвращает значение nil (пустой указатель).

Приведенная ниже программа 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 для работы с драйверами баз данных.

5.4. Использование библиотеки из программы на языке 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++ отсутствуют открытые массивы в параметрах подпрограмм. Тем не менее, программист может вызывать такие подпрограммы, основываясь на том, что открытый массив неявно состоит из двух параметров: указателя на начало массива и номера последнего элемента.

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

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

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

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

Инициализация библиотеки происходит при ее подключении к программе и состоит в выполнении секций 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 получает один целочисленный аргумент, который уточняет причину вызова. Возможные значения аргумента:

  • DLL_PROCESS_DETACH — отключение программы;
  • DLL_PROCESS_ATTACH — подключение программы;
  • DLL_THREAD_ATTACH — создание параллельного потока;
  • DLL_THREAD_DETACH — завершение параллельного потока.

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

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

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

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

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

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

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

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

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

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

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

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

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

5.10. Итоги

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


предыдущий урок содержание семинара следующий урок




Смотрите также материалы по темам:
[Использование и создание DLL] [Подключение DLL, написанных на других языках]

 Обсуждение материала [ 05-04-2007 09:19 ] 6 сообщений
  
Время на сайте: GMT минус 5 часов

Если вы заметили орфографическую ошибку на этой странице, просто выделите ошибку мышью и нажмите Ctrl+Enter.
Функция может не работать в некоторых версиях броузеров.

Web hosting for this web site provided by DotNetPark (ASP.NET, SharePoint, MS SQL hosting)  
Software for IIS, Hyper-V, MS SQL. Tools for Windows server administrators. Server migration utilities  

 
© При использовании любых материалов «Королевства Delphi» необходимо указывать источник информации. Перепечатка авторских статей возможна только при согласии всех авторов и администрации сайта.
Все используемые на сайте торговые марки являются собственностью их производителей.

Яндекс цитирования