Инициализация и завершение работы DLL
Лабораторная работа №10
Динамические библиотеки
Содержание
Содержание. 1
Задание. 1
Проект DLL.. 1
Экспорт из DLL.. 2
Соглашения о вызовах. 3
Инициализация и завершение работы DLL.. 4
Вызов DLL.. 4
Неявный вызов. 5
Явный вызов. 6
Ресурсы в DLL.. 7
Использование модуля ShareMem.. 7
Вызов dll-библиотеки из Visual C++. 7
Задание
Разработать две динамические библиотеки и приложение их использующие. Каждая из библиотек должна содержать по две функции копирования файлов из лабораторной работы №6. Параметры функций: имя файла приемника, имя файла источника. Для вызова функций первой библиотеки использовать явный вызов, для вызова функций второй библиотеки использовать неявный вызов.
Библиотеки должны быть разработаны на Delphi, приложение, вызывающее их, на Visual C++. (Обратить внимание на формат хранения параметров в стеке.)
Проект DLL
Для создания динамической библиотеки в Delphi имеется специальный шаблон. Его значок DLL Wizard расположен на странице New окна создания нового проекта. В отличие от проекта обычного приложения, проект DLL состоит всего из одного исходного файла. Впоследствии к нему можно добавлять отдельные модули и формы.
Для определения типа проекта используется ключевое слово library (вместо program в обычном проекте). При компиляции такого проекта динамической библиотеки создается файл с расширением dll.
Как и в любом другом проекте, в проекте динамической библиотеки можно использовать иные модули. Это могут быть просто модули с исходным кодом и модули форм. При этом динамическая библиотека может экспортировать функции, описанные не только в главном файле, но и в присоединенных модулях.
Блок begin..end называется блоком инициализации библиотеки и предназначен для размещения кода, который автоматически выполняется при загрузке DLL.
Между секцией uses и блоком инициализации можно располагать исходный код функций динамической библиотеки и их объявления. При этом можно использовать любые конструкции языка Object Pascal, а также применять формы и компоненты.
Примечание
При создании динамических библиотек очень удобно использовать группы проектов. В группу помещается проект приложения и проект (проекты) необходимой для его работы динамической библиотеки (библиотек). Для переключения между проектами удобно использовать Диспетчер проектов (команда Project Manager из меню View). Его можно поместить в окно Редактора кода.
Еще один способ удобной работы с проектами динамических библиотек заключается в задании для DLL вызывающей программы. Это делается в диалоге команды Parameters из меню Run. Вызывающее приложение задается в группе Host Application. В результате после компиляции динамической библиотеки вызывается использующее ее приложение.
Для того чтобы приложения могли применять функции динамической библиотеки, необходимо, во-первых, экспортировать их из DLL; во-вторых, объявить функции в самом приложении как внешние. Ниже рассматриваются способы решения этих задач.
Экспорт из DLL
Для создания перечня экспортируемых из динамической библиотеки процедур и функций используется ключевое слово exports. При этом можно указывать как функции, описанные в главном файле DLL, так и функции из присоединенных модулей.
В качестве примера рассмотрим исходный код динамической библиотеки DataCheck, простейшие функции которой проверяют введенную строку перед конвертацией на соответствие одному из типов данных.
library DataChek;
uses
Windows, SysUtils, Classes, Messages, Forms, Dialogs, StdCtrls, ComCtrls;
function ValidDate(AText: String): Integer;
begin
try
Result := 0;
StrToDate(AText);
except
on E:EConvertError do
Result := -1;
end;
end;
function ValidTime(AText: String): Integer;
begin
try
Result := 0;
StrToTime(AText);
except
on E:EConvertError do
Result := -1;
end;
end;
function Validlnt(AText: String): Integer;
begin
try
Result := 0;
StrToInt(AText);
except
on E:EConvertError do
Result := -1;
end;
end;
exports
Validlnt,
ValidDate index 1,
ValidTime index 2 name 'IsValidTime';
begin
if Length(DateToStr(Date)) < 10 then
ShowMessage('Ãîä ïðåäñòàâëåí äâóìÿ öèôðàìè');
end.
Итак, три функции этой библиотеки обеспечивают проверку строки перед преобразованием ее в целое число, дату или время. Для обеспечения экспорта этих функций их необходимо объявить в секции exports.
При компиляции библиотеки адрес, имя и порядковый номер экспортируемой функции добавляется к специальной таблице экспорта в файле DLL.
Имена процедур и функций в секции экспорта разделяются запятыми. Внимательный взгляд на пример экспорта в листинге 1 обнаруживает три различных варианта объявления.
В первом варианте компилятор самостоятельно определяет положение функции в таблице экспорта.
При использовании ключевого слова index следующее за ним число задает положение функции в таблице экспорта относительно других таких же функций.
Ключевое слово name позволяет экспортировать функцию под другим именем.
Соглашения о вызовах
При объявлении процедур и функций в динамических библиотеках используются различные соглашения о вызовах. Дело в том, что различные языки программирования по-разному реализуют передачу параметров в процедуру (через стек или регистры). Порядок следования параметров в стеке как раз определяется соглашением о вызовах.
Стандартный вызов в языках C++ и Object Pascal различается, но набор директив смены типа вызова позволяет обеспечить любую реализацию.
Во всех соглашениях о вызовах вызывающая процедура помещает параметры в стек. В зависимости от типа соглашения, очистка стека осуществляется вызывающей или вызываемой процедурой.
Если очистка стека выполняется вызывающей процедурой, то она успевает забрать из него возвращаемые значения.
Если очистка стека осуществляется вызываемой процедурой, то перед этим она помещает возвращаемые значения во временную область памяти.
Помимо рассмотренных ниже директив имеются еще три типа вызовов, которые не используются и сохранены для обеспечения обратной совместимости. Это директивы near, far, export.
Директива | Описание |
register | Эта директива используется по умолчанию. Поэтому нет необходимости добавлять ключевое слов register после объявления функции. Вызов такого типа называется быстрым (fast call). В нем используются три расширенных регистра процессора, в которые помещаются переменные длиной не более 32-х разрядов и указатели. Остальные параметры помещаются в стек слева направо. После использования стек очищается вызываемой процедурой. |
pascal | Реализует вызовы в стиле языка Pascal. За очистку стека отвечает вызываемая процедура. Параметры помещаются в стек слева направо. Этот способ вызова является очень быстрым, но не поддерживает переменное число параметров. Используется для обеспечения обратной совместимости. |
stdcall | Параметры помещаются в стек слева направо. Очистка стека осуществляется вызываемой процедурой. Этот вызов обеспечивает обработку фиксированного числа параметров. |
cdecl | Реализует вызовы в стиле языка С. Параметры в стек помещаются справа налево. Очистка стека осуществляется вызывающей процедурой. Такие вызовы обеспечивают обслуживание переменного числа параметров, но скорость обработки меньше, чем в вызовах при реализации директивы pascal. Эта директива в основном применяется для обращения к динамическим библиотекам, использующим соглашения о вызовах в стиле языка С. Использование директивы cdecl для библиотек Delphi не вызовет ошибку компиляции, но переменное число параметров не обеспечит. |
safecall | Параметры помещаются в стек справа налево. Очистка стека осуществляется вызываемой процедурой. Используется в СОМ и основанных на ней технологиях. |
Инициализация и завершение работы DLL
При загрузке динамической библиотеки выполняется код инициализации, который расположен в блоке begin, .end. Обычно здесь выполняются операции по заданию начальных значений используемых в функциях библиотеки переменных, проверка условий функционирования DLL, создание необходимых структур и объектов и т. д.
При загрузке динамической библиотеки в адресное пространство вызывавшего ее процесса, происходят важные события, знание которых позволит вам эффективно управлять инициализацией и выгрузкой DLL.
Итак, перед запуском кода инициализации автоматически вызывается встроенная ассемблерная процедура _initDLL (она расположена в модуле system). Она сохраняет состояние регистров процессора; получает значение экземпляра модуля библиотеки и записывает его в глобальную переменную hinstance; устанавливает для глобальной переменой isLibrary значение True (по этому значению вы всегда сможете распознать код DLL); получает из стека ряд параметров; проверяет переменную процедурного типа DLLProc:
var DLLProc: Pointer;
Эта переменная используется для проверки вызовов операционной системой точки входа DLL. С этой переменной можно связать процедуру с одним целочисленным параметром. Такая процедура называется функцией обратного вызова системного уровня.
При завершении работы динамической библиотеки вызывается процедура, на которую указывает адрес, содержащийся в переменной ExitProc:
var ExitProc: Pointer;
Вызов DLL
Теперь рассмотрим, как вызываются функции из динамических библиотек.
При запуске исполняемого файла приложения операционная система создает для его работы отдельный процесс. Также система создает первичный поток, владельцем которого является процесс. Процесс приложения получает 4 Гбайт адресного пространства, в которое отображается исполняемый код приложения.
После этого из исполняемого кода извлекается информация обо всех вызываемых приложением динамических библиотеках и их функциях. Эта информация основывается на анализе исходного кода компоновщиком Delphi, который включает в исполняемый файл имена функций и динамических библиотек. При этом используется неявный вызов, описываемый ниже.
В результате при обращении приложения к функции из DLL вся информация о ней уже имеется в процессе. Для выполнения функции (вызов осуществляется одним из потоков процесса приложения) в адресное пространство процесса приложения загружается соответствующая динамическая библиотека. После этого исполняемый код DLL становится полностью доступен внутри процесса, но не вне его. Другие Процессы могут загрузить эту же библиотеку и использовать ее образ в собственном адресном пространстве. Именно поэтому несколько приложений могут применять одну динамическую библиотеку одновременно.
Каждый поток имеет собственный стек, в который загружаются параметры функций DLL и все необходимые локальные переменные. Дело в том, что динамические библиотеки не имеют собственной кучи и не могут владеть данными. Поэтому любые создаваемые функциями DLL данные или объекты принадлежат вызывавшему потоку.
Функции динамических библиотек могут вызываться двумя способами — явным и неявным. Рассмотрим их.
Неявный вызов
Механизм неявного вызова наиболее прост, т.к. выполняется автоматически и основан на имеющейся в приложении информации о вызываемых функциях и динамических библиотеках. Однако разработчик не имеет возможности влиять на ход загрузки DLL. Если операционная система не смогла загрузить библиотеку, просто выдается сообщение об ошибке. Единственный способ повлиять на процесс загрузки - использовать секцию инициализации библиотеки (см. выше).
В качестве примера неявного вызова рассмотрим простое приложение DemoDLLl, использующее функции библиотеки DataCheck (см. выше). Для этого в нем имеются три компонента TEdit, в которых осуществляется проверка введенной строки на соответствие формату одного из типов данных.
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, comctrls, Buttons;
type
TMainForm = class(TForm)
Edit1: TEdit;
Edit2: TEdit;
Edit3: TEdit;
Label1: TLabel;
Label2: TLabel;
Label3: TLabel;
procedure Edit1Exit(Sender: TObject);
procedure Edit2Exit(Sender: TObject);
procedure Edit3Exit(Sender: TObject);
private { Private declarations }
public { Public declarations }
end;
var
MainForm: TMainForm;
function IsValidInt(AText: String): Boolean; external 'DataCheck.dll';
function IsValidDate(AText: String): Boolean; external 'DataCheck.dll';
function ValidTime(AText: String): Boolean; external 'DataCheck.dll';
implementation {$R *.DFM}
procedure TMainForm.Edit1Exit(Sender: TObject);
begin
if not IsValidInt(Edit1.Text) then Edit1.Clear;
end;
procedure TMainForm.Edit2Exit(Sender: TObject);
begin
if not IsValidDate(Edit2.Text) then Edit2.Clear;
end;
procedure TMainForm.Edit3Exit(Sender: TObject);
begin
if not ValidTime(Edit3.Text) then Edit3.Clear;
end;
end.
Для организации неявного вызова достаточно объявить нужную функцию с директивой external и указать имя содержащей ее динамической библиотеки. Обратите внимание, что третья функция объявлена под псевдонимом IsValidTime, который объявлен для этой функции при помощи ключевого слова name в исходном коде динамической библиотеки.
В дальнейшем импортированные функции используются обычным образом.
Явный вызов
Явный вызов динамической библиотеки подразумевает создание программистом соответствующего исходного кода. Ему необходимо предусмотреть загрузку DLL, получение адресов переменных процедурного типа для используемых функций и процедур, выгрузку DLL.
unit Unit2;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls;
type
StandardProc = function(AText: String): Boolean;
TMainForm = class(TForm)
Edit1: TEdit;
Edit2: TEdit;
Edit3: TEdit;
Label1: TLabel;
Label2: TLabel;
LabelS: TLabel;
procedure FormShow(Sender: TObject);
procedure FormClose(Sender: TObject; var Action: TCloseAction);
procedure Edit1Exit(Sender: TObject);
procedure Edit2Exit(Sender: TObject);
procedure Edit3Exit(Sender: TObject);
private
DLLHandle: THandle;
LoadError: Word;
IsValidInt: StandardProc;
IsValidDate: StandardProc;
ValidTime: StandardProc;
end;
var
MainForm: TMainForm;
implementation {$R *.DFM}
procedure TMainForm.FormShow(Sender: TObject);
begin
DLLHandle := LoadLibrary('DataCheck');
if DLLHandle = 0 then
begin
if GetLastError = ERROR_DLL_NOT_FOUND then
ShowMessagef'Îøèáêà çàãðóçêè DLL');
Close;
end;
@IsValidInt:=GetProcAddress(DLLHandle, 'IsValidInt');
@SIsValidDate:=GetProcAddress(DLLHandle, 'IsValidDate');
@SValidTime:=GetProcAddress(DLLHandle, 'ValidTime');
end;
procedure TMainForm.FormClose(Sender: TObject;
var Action: TCloseAction);
begin
if DLLHandle <> 0 then FreeLibrary(DLLHandle);
end;
procedure TMainForm.Edit1Exit(Sender: TObject);
begin
if not IsValidInt(Edit1.Text) then Edit1.Clear;
end;
procedure TMainForm.Edit2Exit(Sender: TObject);
begin
if not IsValidDate(Edit2.Text) then Edit2.Clear;
end;
procedure TMainForm.Edit3Exit(Sender: TObject);
begin
if not ValidTime(Edit3.Text) then Edit3.Clear;
end;
end.
Загрузка динамической библиотеки DataCheck осуществляется в методе-обработчике FormShow при помощи функции LoadLibrary. Имя динамической библиотеки может не содержать маршрута, если файл DLL расположен в одном каталоге с программой. Если в этом каталоге файл DLL не найден, поиск последовательно проводится в текущем каталоге, \SYSTEM и каталогах из перечня Path.
Так как для этой системной функции не создается исключительная ситуация, то следом предусмотрен контроль возможных ошибок. Функция GetLastError возвращает код последней ошибки.
Если библиотека успешно загружена, в три процедурные переменные типа standardProc передаются адреса соответствующих функций DLL. Процедурный тип standardProc объявлен перед классом формы. Для этого используется системная функция GetProcAddress.
В дальнейшем созданные таким образом функции применяются для вводимых значений в компонентах TEdit.
При закрытии приложения необходимо выгрузить все используемые динамические библиотеки при помощи системной функции FreeLibrary.
Ресурсы в DLL
Динамические библиотеки могут содержать не только исполняемый код, проводящий некоторые вычисления, но и ресурсы. Чаще всего бывает необходимо распространять вместе с DLL формы, обеспечивающие работу процедур и функций. Приемы работы с формами в проектах динамических библиотек ничем не отличаются от тех же приемов в проектах обычных приложений.