Главная arrow В помощь студентам arrow Статьи по программированию в Delphi arrow Программирование на языке Delphi. Глава 5. Динамически загружаемые библиотеки. Часть 1  
23.02.2020 г.
Программирование на языке Delphi. Глава 5. Динамически загружаемые библиотеки. Часть 1 Печать E-mail
Автор Administrator   
12.03.2009 г.

Оглавление

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

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

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

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

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

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

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

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

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

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

В стандартном случае экспортное имя подпрограммы считается в точности таким, как ее идентификатор в исходном тексте библиотеки (с учетом заглавных и строчных букв). Например, если секция 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';

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

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

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

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

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

 

ПРИМЕЧАНИЕ

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

 

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

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

Директива

Порядок занесения аргументов в стек

Кто отвечает за очистку стека

Передача аргументов через регистры

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

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

 

ПРИМЕЧАНИЕ

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

 

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

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

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

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

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

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

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

Рисунок 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 (рисунок 2).

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

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

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

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

Последнее обновление ( 12.03.2009 г. )
 
« Пред.   След. »

Ivanovo State University of Chemical Technology has entered into an academic partnership with Visual Paradigm to better facilitate the teaching of software design & modeling through the use of Visual Paradigm.
Enterprise Architect
Sparx Systems Enterprise Arctitect provides Ivanovo State University of Chemical Technology with Enterprise Architect, Eclipse Integration, Visual Studio Integration, SysML Technology, Zachman Framework and much more for use in educational purposes, offered by the Enterprise Architect Academic Site License.