Отправляет email-рассылки с помощью сервиса Sendsay

Программирование на Delphi

  Все выпуски  

Программирование на DELPHI v3-7 Всё о ДЛЛ(Часть 2)


Информационный Канал Subscribe.Ru

Программирование на Delphi
подписчиков: +8500

www.goldfaq.ru   forum.goldfaq.ru

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

Теперь о дизайне: Как вы уже, наверное, успели заметить, он изменился, это связано со сменой дизайна сайта. Что вы думаете по этому поводу?

О вопросах: Нумерация вопросов с этого выпуска начинается заново, т.е с 1.

О сайте и форуме: Наш сайт(http://www.GoldFAQ.ru) теперь работает на движке, и вы сами можете добавлять статьи и файлы! Рекомендую всем посетить наш форум!

О статье выпуска: Я решил поместить в этот выпуск замечательную статью Семенова Кирилла "Всё о DLL". Она является настоящим кладом для новичков и хорошим справочником для продвинутых программистов! Но, т.к статья слишком большая для одного выпуска, мне пришлось сделать 2 части рассылки.

  
Кто ещё не зарегистрировался в нашем форуме, просим пройти сюда:
http://forum.goldfaq.ru - спешите занять рульные ники ;)

Delphi-Статьи

Введение 

В связи с бурным развитием технологий программирования, все больше людей сталкиваются с проблемой наращивания возможностей своих программ. Данная статья посвящена именно этому вопросу, а именно - программирование DLL в Borland Delphi. Кроме того, так как мы затронем вопросы по использованию библиотек DLL, то попутно коснемся импортирования функций из чужих DLL (в том числе и системных, т.е. WinAPI).

Области применения DLL 

Итак, зачем же нужны библиотеки DLL и где они используются?.. Перечислим лишь некоторые из областей их применения:

  • Отдельные библиотеки, содержащие полезные для программистов дополнительные функции. Например, функции для работы со строками или же - сложные библиотеки для преобразования изображений.
  • Хранилища ресурсов. В DLL можно хранить не только программы и функции, но и всевозможные ресурсы - иконки, рисунки, строковые массивы, меню и т.д.
  • Библиотеки поддержки. В качестве примера можно привести библиотеки таких известных пакетов, как: DirectX, ICQAPI (API для ICQ), OpenGL и т.д.
  • Части программы. Например, в DLL можно хранить окна программы (формы) и т.п.
  • Плагины (Plugins). - Вот где настоящий простор для мыслей программиста! Плагины - дополнения к программе, расширяющие ее возможности. Например, в этой статье мы рассмотрим теорию создания плагина для собственной программы.
  • Разделяемый ресурс. DLL (Dynamic Link Library) может быть использована сразу несколькими программами или процессами (т.н. sharing - разделяемый ресурс)

Краткое описание функций и приемов для работы с DLL

 

Итак, какие же приемы и функции необходимо использовать, чтобы работать с DLL? Разберем два метода импортирования функций из библиотеки:

1 способ. Привязка DLL к программе. Это наиболее простой и легкий метод для использования функций, импортируемых из DLL. Однако (и на это следует обратить внимание), этот способ имеет очень весомый недостаток - если библиотека, которую использует программа, не будет найдена, то программа просто не запустится, выдавая ошибку и сообщая о том, что ресурс DLL не найден. А поиск библиотеки будет вестись: в текущем каталоге, в каталоге программы, в каталоге WINDOWS\SYSTEM, и т.д.
Итак, для начала - общая форма этого приема:

implementation
...
function FunctionName(Par1: Par1Type; Par2: Par2Type; ...): ReturnType; stdcall; external 'DLLNAME.DLL' name 'FunctionName' index FuncIndex;
// или (если не функция, а процедура):
procedure ProcedureName(Par1: Par1Type; Par2: Par2Type; ...); stdcall; external 'DLLNAME.DLL' name 'ProcedureName' index ProcIndex;

Здесь: FunctionName (либо ProcedureName) - имя функции (или процедуры), которое будет использоваться в Вашей программе;
Par1, Par2, ... - имена параметров функции или процедуры;
Par1Type, Par2Type, ... - типы параметров функции или процедуры (например, Integer);
ReturnType - тип возвращаемого значения (только для функции);
stdcall - директива, которая должна точно совпадать с используемой в самой DLL;
external 'DLLNAME.DLL' - директива, указывающая имя внешней DLL, из которой будет импортирована данная функция или процедура (в данном случае - DLLNAME.DLL);
name 'FunctionName' ('ProcedureName') - директива, указывающая точное имя функции в самой DLL. Это необязательная директива, которая позволяет использовать в программе функцию, имеющую название, отличное от истинного (которое она имеет в библиотеке);
index FunctionIndex (ProcedureIndex) - директива, указывающая порядковый номер функции или процедуры в DLL. Это также необязательная директива.

2 способ. Динамическая загрузка DLL. Это гораздо более сложный, но и более элегантный метод. Он лишен недостатка первого метода. Единственное, что неприятно - объем кода, необходимого для осуществления этого приема, причем сложность в том, что функция, импортируемая из DLL достуна лишь тогда, когда эта DLL загружена и находится в памяти... С примером можно ознакомиться ниже, а пока - краткое описание используемых этим методом функций WinAPI:

LoadLibrary(LibFileName: PChar) - загрузка указанной библиотеки LibFileName в память. При успешном завершении функция возвращает дескриптор (THandle) DLL в памяти.
GetProcAddress(Module: THandle; ProcName: PChar) - считывает адpес экспоpтиpованной библиотечной функции. При успешном завершении функция возвращает дескриптор (TFarProc) функции в загруженной DLL.
FreeLibrary(LibModule: THandle) - делает недействительным LibModule и освобождает связанную с ним память. Следует заметить, что после вызова этой процедуры функции данной библиотеки больше недоступны.

Практика и примеры 

Ну а теперь пора привести пару примеров использования вышеперечисленных методов и приемов: 

Пример 1. Привязка DLL к программе 

{... Здесь идет заголовок файла и определение формы TForm1 и ее экземпляра Form1} 

implementation 

{Определяем внешнюю библиотечную функцию} 

function GetSimpleText(LangRus: Boolean): PChar; stdcall; external 'MYDLL.DLL'; 

procedure Button1Click(Sender: TObject);

begin

{И используем ее}

 ShowMessage(StrPas(GetSimpleText(True)));

 ShowMessage(StrPas(GetSimpleText(False)));

 {ShowMessage - показывает диалоговое окно с указанной надписью; StrPas - преобразует строку PChar в string}

end;

Теперь то же самое, но вторым способом - с динамической загрузкой:

Пример 2. Динамическая загрузка DLL

{... Здесь идет заголовок файла и определение формы TForm1 и ее экземпляра Form1}

var

      Form1: TForm1;

      GetSimpleText: function(LangRus: Boolean): PChar;

      LibHandle: THandle;

procedure Button1Click(Sender: TObject);

begin

  {"Чистим" адрес функции от "грязи"}

  @GetSimpleText := nil;

  {Пытаемся загрузить библиотеку}

  LibHandle := LoadLibrary('MYDLL.DLL');

  {Если все OK}

  if LibHandle >= 32 then begin

    {...то пытаемся получить адрес функции в библиотеке}

    @GetSimpleText := GetProcAddress(LibHandle,'GetSimpleText');

    {Если и здесь все OK}

    if @GetSimpleText <> nil then

      {...то вызываем эту функцию и показываем результат}

      ShowMessage(StrPas(GetSimpleText(True)));

  end;

  {И не забываем освободить память и выгрузить DLL}

  FreeLibrary(LibHandle);

end;

ПРИМЕЧАНИЕ: Следует воздерживаться от использования типа string в библиотечных функциях, т.к. при его использовании существуют проблемы с "разделением памяти". Подробней об этом можно прочитать (правда, на английском) в тексте пустого проекта DLL, который создает Delphi (File -> New -> DLL). Так что лучше используйте PChar, а затем при необходимости конвертируйте его в string функцией StrPas.

Ну а теперь разберем непосредственно саму библиотеку DLL:

Пример 3. Исходник проекта MYDLL.DPR

library mydll;

uses SysUtils, Classes;

{Определяем функцию как stdcall}

function GetSimpleText(LangRus: Boolean): PChar; stdcall;

begin

  {В зависимости от LangRus возвращаем русскую (True) либо английскую (False) фразу}

  if LangRus then

    Result := PChar('Здравствуй, мир!')

  else

    Result := PChar('Hello, world!');

end;

{Директива exports указывает, какие функции будут экспортированы этой DLL}

exports GetSimpleText;

begin

end.

Размещение в DLL ресурсов и форм

В DLL можно размещать не только функции, но и курсоры, рисунки, иконки, меню, текстовые строки. На этом мы останавливаться не будем. Замечу лишь, что для загрузки ресурса нужно загрузить DLL, а затем, получив ее дескриптор, - загружать сам ресурс соотвествующей функцией (LoadIcon, LoadCursor, и т.д.). В этом разделе мы лишь немного затронем размещение в библиотеках DLL окон приложения (т.е. форм в Дельфи).

Для этого нужно создать новую DLL и добавить в нее новую форму (File -> New -> DLL, а затем - File -> New Form). Далее, если форма представляет собой диалоговое окно (модальную форму (bsDialog)), то добавляем в DLL следующую функцию (допустим, форма называется Form1, а ее класс - TForm1):

 

Пример 4. Размещение формы в DLL

function ShowMyDialog(Msg: PChar): Boolean; stdcall;

...

exports ShowMyDialog;

 

function ShowMyDialog(Msg: PChar): Boolean;

begin

  {Создаем экземпляр Form1 формы TForm1}

  Form1 := TForm1.Create(Application);

  {В Label1 выводим Msg}

  Form1.Label1.Caption := StrPas(Msg);

  {Возвращаем True только если нажата OK (ModalResult = mrOk)}

  Result := (Form1.ShowModal = mrOk);

  {Освобождаем память}

  Form1.Free;

end;

 

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

Создание плагинов

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

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

 

Часть 2. Создание и использование динамически загружаемых библиотек в Delphi

Создание простейшей DLL. Соглашения о вызовах методов
Статическая и динамическая загрузка DLL
Обмен данными с DLL
Вызов методов приложения в DLL
Модальные формы в DLL
Немодальные формы в DLL
Экспорт дочерних форм из DLL
Заключение

Динамически загружаемые библиотеки (dynamic-link libraries, DLL)  являются, пожалуй, одним из наиболее мощных средств создания приложений в Windows. По структуре данных DLL напоминает приложение — exe-файл, но в отличие от *.exe-приложения код в DLL не может выполняться самостоятельно. DLL (как и *.exe-файл) можно загрузить в память компьютера, и работающие приложения могут вызвать методы, экспонируемые в DLL. На основе DLL создаются также элементы управления ActiveX.

Рассмотрим преимущества использования DLL:

1. Методы, описанные в DLL, могут одновременно обслуживать несколько приложений. При этом сами методы хранятся в виде одной копии в ОЗУ. Если вызываемый код достаточно велик и имеется несколько приложений, которые вызывают данный код, то вследствие этого можно достичь существенной экономии системных ресурсов.

2. Возможность хранения общих ресурсов. Если несколько приложений работают с одними и теми же ресурсами (например, большие растровые картинки — *.bmp), то при сохранении их в DLL можно иметь эти ресурсы в одном экземпляре.

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

4. Возможно использовать различные языки программирования для создания *.exe и *.dll. Например, *.ex-файл может компилироваться из кода, написанного на Delphi, а *.dll, которая им используется, компилируется из кода, написанного на Microsoft Visual C++. Если приложение использует несколько DLL, то они могут быть созданы на различных языках программирования. Это значительно упрощает перенос кода в другие приложения.

5. DLL можно загружать в память только тогда, когда они требуются для выполнения приложений, реализуя тем самым динамическую загрузку. При этом опять же достигается экономия системных ресурсов. Так, например, в DLL можно реализовать диалог, с помощью которого изменяются какие-либо параметры приложения. Пользователь может месяцами не обращаться к данному диалогу, и при этом DLL, в которой он реализован, не загружается в память и не потребляет системных ресурсов. Только в момент, когда пользователь обращается к этому диалогу, происходит загрузка DLL в память, но после выполнения диалога эта память освобождается. Используя динамическую загрузку, можно оформить динамический пользовательский интерфейс:соответствующие опции меню приложения появляются в том случае, если найдена данная DLL, и исчезают при ее отсутствии. Такой интерфейс удобен при поставке приложений, в которых пользователь может заказать дополнительные возможности за отдельную плату.

Создание простейшей DLL. Соглашения о вызовах методов

В состав Delphi входит эксперт для создания DLL, который вызывается при выборе команды File/New и пиктограммы DLL на странице  New репозитария объектов. При этом возникает заготовка для реализации DLL:

library FirstLib; 

uses 

  SysUtils, 

  Classes; 

begin 

end. 

В приведенном выше коде отсутствует текстовый комментарий, который генерируется экспертом. Заготовка отличается от заготовки для создания кода *.exe-файла тем, что используется служебное слово Library вместо Program. Кроме того, отсутствуют обращение к методам объекта TApplication (хотя экземпляр этого объекта в действительности создается в DLL!), а также модуль реализации главной формы. Создадим в данной заготовке метод, который будет симулировать выполнение каких-либо вычислений:

function AddOne(N:integer):integer; stdcall; export

begin 

  Result:=N+1; 

end; 

Как видно, код реализации метода AddOne включает два оператора, которые обычно не используются в реализации методов при создании приложения, — stdcall и export. Директива stdcall связана с соглашениями вызова методов. Рассмотрим их здесь подробнее.

Когда в приложении осуществляется вызов метода, его параметры (как и локальные переменные) помещаются в стек. Стек, представляющий собой зарезервированное место в ОЗУ компьютера, имеет указатель текущей позиции, который при старте приложения устанавливается на начало стека. При вызове метода в стек помещаются все локальные переменные и параметры метода, при этом указатель текущей позиции стека смещается вправо в соответствии с размером помещаемых в него данных. Если метод, в свою очередь, вызывает другой метод, то локальные переменные второго метода добавляются в стек, так же как и список параметров. После окончания работы второго метода происходит освобождение области памяти в стеке — для этого указатель текущей позиции стека смещается влево. И наконец, после окончания работы первого метода указатель текущей позиции стека смещается в первоначальное положение. Сказанное иллюстрируется

Ясно, что если приложение работает нормально, то после окончания выполнения цепочки методов указатель текущей позиции стека должен вернуться в первоначальное состояние, то есть  созданный стек должен быть очищен (stack cleanup). Если же указатель не возвращается, то происходит крах стека (stack crash) — этот термин не следует путать с очисткой стека. В этом случае приложение прекращает свою работу (никакие ловушки исключений не помогают) и, если оно выполняется под Windows 95 или Windows 98, чаще всего требуется перезагрузка операционной системы. Понятно, что возврат указателя стека в первоначальное состояние должен происходить по окончании работы метода. Но при этом существуют две возможности — возврат указателя на место может производить как вызываемый метод по окончании работы, так и вызывающий метод после завершения работы вызываемого метода. В принципе, в различных языках программирования реализуются обе указанные возможности — очищать стек могут и вызванный, и вызывающий методы. Поскольку модуль пишется на одном языке программирования, то эти проблемы скрыты от программиста: очистка стека производится по специфичному для данного языка протоколу. Но если используются различные модули, код для которых реализован на различных языках программирования, то возникают проблемы. Например, в C++ стек очищается в методе, который вызвал второй метод, после окончания его работы. В Delphi же стек очищается в том же самом методе, где он используется, перед окончанием его работы. Если *.exe-модуль, созданный на языке C++, вызывает метод из DLL, созданный на Delphi, то перед окончанием работы метода в DLL стек будет очищен. После этого управление передается модулю, реализованном на C++, который также попытается очистить стек, — такое действие приведет к краху стека.

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

procedure DoSomething(N:integer; D:TDateTime);

Указанный способ заключается в том, что сначала в стек может быть помещена константа N, а затем D (слева направо) или вначале помещается константа D, а затем N (справа налево). Кроме того, некоторые языки программирования (в частности, Delphi) часть параметров метода вообще не помещают в стек, а передают их через регистры процессора. К тому же в разных языках программирования параметры могут помещаться в стек как слева направо, так и справа налево. Если они были помещены слева направо, а вызываемый метод будет читать справа налево, то получится путаница: в качестве значения константы N вызываемый метод будет считать значение правой половины константы D, а константу D он будет формировать из константы N и левой половины D.

По этой причине в любом языке программирования предусмотрена возможность объявить, какой из методов — вызываемый или вызывающий,  [U1] будет очищать стек и в какой последовательности параметры метода помещаются в стек. Такое объявление называется соглашением вызова (calling conversion); имеется ряд зарезервированных слов, которые помещаются после заголовка методов, как показано в таблице.

Директива

Порядок следования параметров

Очистка стека

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

register

Слева направо

Вызываемый метод

+

pascal

Слева направо

Вызываемый метод

-

cdecl

Справа налево

Вызывающий метод

-

stdcall

Справа налево

Вызываемый метод

-

safecall

Справа налево

Вызываемый метод

-

Для методов, экспонируемых в DLL, рекомендуется (но не обязательно) использовать то же соглашение вызова, что и в Windows API. Для 32-разрядных приложений Windows API методы реализованы таким образом, что параметры помещаются в стек справа налево и стек очищает вызываемый метод; при этом не используются регистры процессора для передачи данных. Этим условиям удовлетворяет директива stdcall, которая в описанном выше примере помещается после заголовка метода AddOne. Если после заголовка метода отсутствует соглашение о вызове, то по умолчанию Delphi использует соглашение register.

Второе служебное слово в заголовке метода — export — информирует компилятор о том, что код для данного метода должен быть создан таким образом, чтобы его можно было вызывать из других модулей.  Эта директива требуется при реализации DLL в Delphi 3; в Delphi 4 и 5 ее можно опустить.

Однако написанного выше кода еще недостаточно для вызова метода AddOne из другого модуля. Одна DLL может предоставлять несколько методов внешнему модулю. Для того чтобы внешний модуль мог выбрать конкретный метод, в DLL должна присутствовать специальная секция, которая имеет заголовок exports (не путать с директивой export!). В нашем примере эту секцию можно объявить следующим образом:

exports

  AddOne index 1 name ’CalculateSum’; 

Для экспонирования метода в секции exports просто приводится его название (AddOne), после которого следует либо служебное слово index с целочисленным идентификатором после него (идентификатор должен быть больше нуля), либо служебное слово name с текстовым идентификатором, либо оба вместе — как в данном случае. Внешний модуль может обращаться к конкретному методу как по индексу, так и по имени. Как это делается — будет рассказано в следующем разделе. На данном этапе изложения материала следует отметить, что название метода AddOne нигде и никогда не будет видно во внешних модулях — будет использоваться либо целочисленная константа 1, либо имя CalculateSum. Сразу же следует отметить, что имя чувствительно к регистру букв — метод не будет найден, если использовать, например, такие имена, как calculatesum или CALCULATESUM. Индексы, если они объявляются в секции exports, обязаны начинаться с 1 и принимать все целочисленные значения подряд (2, 3, 4…). Нельзя опускать какое-либо число в этой последовательности натуральных чисел — могут быть ошибки при импорте метода по индексу!

Недавно компания Microsoft объявила о том, что DLL должны экспортироваться по имени. Поэтому во вновь создаваемых DLL необходимо объявлять имя метода в секции exports, при этом индексы объявлять не следует (по крайней мере, не использовать их для определения адреса метода).

Статическая и динамическая загрузка DLL

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

При статической загрузке для вызова другого модуля следует в какой-либо из секций описать метод из DLL следующим образом:

function Add1(K:integer):integer; stdcall; external 'FirstLib.dll' name 'CalculateSum'; 

или

function Add1(K:integer):integer; stdcall; external 'FirstLib.dll' index 1; 

Для тестирования необходимо описать в приложении внешний метод одним из вышеупомянутых способов и сделать, например, обработчик события OnClick кнопки, поставленной на форму вместе с компонентом TEdit:

procedure TForm1.Button1Click(Sender: TObject);

var 

  N:integer; 

begin 

  N:=StrToInt(Edit1.Text); 

  N:=Add1(N); 

  Edit1.Text:=IntToStr(N); 

end; 

При нажатии кнопки будет вызываться метод из DLL. Обратите внимание на изменение имен метода: из обработчика события OnClick вызывается метод с именем Add1. Этот метод экспонируется в DLL под именем CalculateSum. В реализации DLL он имеет название AddOne.

При таком определении метода DLL будет загружена немедленно после старта приложения и выгружена вместе с его завершением. В приведенном выше примере следует обратить внимание на то, что после имени динамической библиотеки указано ее расширение (FirstLib.dll). Такая конструкция необходима для загрузки библиотеки в Windows NT, поскольку без расширения *.dll файл не будет найден! В Windows 95 расширение не обязательно.

При поиске DLL для загрузки первоначально определяется, была ли данная DLL уже загружена в память другим модулем. Если была — то извлекается адрес метода и передается приложению. Если же нет — то операционная система начинает ее поиск на диске. При этом, если путь при имени DLL не указан в явном виде, система ищет библиотеку в каталоге модуля, который старается загрузить DLL. Если не находит, то продолжает поиски в директориях WINDOWS и WINDOWS\SYSTEM (или WINNT, WINNT\SYSTEM, WINNT\SYSTEM32). После этого происходит поиск в каталогах, определенных в переменной среды Path. Если библиотека с заданным именем будет найдена, то она загрузится и приложение стартует. Если же нет — происходит исключение и приложение прекратит свою работу. Приложение прекращает работу также и в том случае, если не будет найден метод с данным именем (или индексом, если он импортируется по индексу).

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

type

  TAddFunction=function(K:integer):integer; stdcall; 

  

procedure TForm1.Button2Click(Sender: TObject); 

var 

  Add1:TAddFunction; 

  HLib:THandle; 

  N:integer; 

begin 

  HLib:=0; 

  try 

    HLib:=LoadLibrary('FirstLib.dll'); 

    if HLib>HINSTANCE_ERROR then begin 

      Add1:=GetProcAddress(HLib,'CalculateSum'); 

      if Assigned(Add1) then begin 

        N:=StrToInt(Edit1.Text); 

        N:=Add1(N); 

        Edit1.Text:=IntToStr(N); 

      end else ShowMessage('Method with name CalculateSum was not found'); 

    end else ShowMessage('Can not load library FirstLib.dll'); 

  finally 

    if HLib>HINSTANCE_ERROR then FreeLibrary(HLib); 

  end; 

end; 

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

Далее в коде приложения вызывается метод LoadLibrary, который в качестве параметра использует имя библиотеки. После успешной отработки данного метода и загрузки библиотеки в память указатель на загруженную библиотеку помещается в переменную HLib. Если библиотеку не удается найти (или загрузить), то в эту же переменную помещается код ошибки. Чтобы определить, была ли загружена библиотека, переменную HLib следует сравнить с константой HINSTANCE_ERROR, которая определена в модуле Windows. Если библиотека была успешно загружена, то осуществляется попытка найти адрес метода в памяти компьютера при помощи вызова метода GetProcAddress. Указанный метод возвращает адрес того метода, имя которого указано во втором параметре GetProcAddress. Если же метод не был найден, то возвращается nil-адрес.

Соответственно вызов метода Add1 осуществляется только в случае, если он был успешно найден. Затем, поскольку при загрузке библиотеки были заняты системные ресурсы, их необходимо вновь вернуть операционной системе, выгрузив библиотеку из памяти. Для этого вызывается метод FreeLibrary. При загрузке DLL производится подсчет ссылок, а именно: при каждом успешном обращении к методу LoadLibrary в DLL счетчик ссылок увеличивается на единицу, а при каждом вызове метода FreeLibrary счетчик ссылок уменьшается на единицу. Как только счетчик ссылок станет равным нулю, библиотека выгружается из памяти компьютера. Следовательно, каждому успешному вызову LoadLibrary должно соответствовать обращение к FreeLibrary — иначе DLL не выгрузится из памяти компьютера даже после окончания работы приложения. Поэтому данные методы были помещены в защищенный блок try…finally..end; — это гарантирует вызов метода FreeLibrary, если происходит исключение.

Метод GetProcAddress для загрузки DLL может также использовать индексы. Для примера, представленного выше, в котором метод AddOne экспонируется с индексом 1, использование индексов в GetProcAddress выглядит следующим образом:

Add1:=GetProcAddress(HLib,pchar(1)); 

Помните, что при использовании индексов следует соблюдать осторожность. Для корректного использования индексов необходимо, чтобы все методы в DLL были проиндексированы с значениями индексов от 1 до N (N — число методов, обьявленных в секции exports). Если какой-либо индекс опускается (в этом случае максимальное значение индекса будет больше числа методов), то GetProcAddress возвращает ненулевой адрес для несуществующего индекса! Ясно, что такой адрес является недействительным и при попытке обращения к нему генерируется исключение. По-видимому, по причине некорректной работы метода GetProcAddress Microsoft запрещает использовать индексы для импорта методов из DLL.

Теперь следует рассмотреть, каким образом загружаемая DLL размещается в ОЗУ компьютера. При загрузке DLL осуществляется резервирование памяти, необходимое для хранения кода методов. Кроме того, резервируется место для всех глобальных переменных и выполняются секции инициализации в модулях DLL. Если другой процесс также пытается загрузить DLL, то вновь происходит резервирование памяти для хранения глобальных переменных. Однако копирование методов DLL не осуществляется;  также не выполняется и секция инициализации. Другими словами, одна копия метода в ОЗУ обслуживает несколько приложений. Глобальные переменные являются уникальными для каждого приложения, и если одно приложение изменит их значение при помощи вызова какого-нибудь метода, то другое приложение этого не заметит. Поскольку секция инициализации выполняется только при первой загрузке DLL, ее нельзя использовать для установки начальных значений глобальных переменных. Вышесказанное необходимо  учитывать при разработке DLL.

Обмен данными с DLL

DLL имеет общее адресное пространство с приложением, из которого вызываются его методы. Из этого следует, что указатель на какой-либо объект в памяти DLL является легальным внутри приложения, и наоборот. Это позволяет передать, например, адрес метода или адрес данных, чего нельзя сделать без использования маршрутизации при взаимодействии двух приложений. Имеется, однако, существенное отличие от передачи данных между двумя методами одного модуля: в различных модулях разными являются и диспетчеры памяти (memory manager). Это означает, что если в каком-то модуле (например, в DLL) был вызван метод GetMem, то освободить системные ресурсы вызовом метода FreeMem можно только в том же самом модуле. Если попытаться вызвать метод FreeMem в приложении (для примера выше), то происходит исключение. Поскольку при создании экземпляров класса также происходит обращение к диспетчеру памяти, то их деструкторы нельзя вызвать за пределами модуля. В частности, если в DLL происходит исключение, то создается объект в диспетчере памяти DLL. Если не использовать ловушки исключений, то этот объект попадает в приложение и после показа пользователю сообщения приложение попытается его разрушить. В результате вновь произойдет исключение. Поэтому все экспонируемые в DLL методы, в которых могут произойти исключения, должны иметь ловушку исключения:

try

  {Main code should inserted here} 

  {Resource cleanup here} 

except 

  On E:exception do begin 

    ShowMessage(E.Message); 

    {Resource cleanup. Do not use RAISE directive here!}  

  end; 

end; 

Следует иметь в виду, что ни в коем случае нельзя использовать директиву Raise в секции except…end! В этой секции нужно просто показать пользователю сообщение о возникновении исключения (или не показывать его, если условия работы приложения позволяют сделать это).

По этой же причине (различные диспетчеры памяти) нельзя использовать строки Delphi для передачи данных между модулями. Строки Delphi — это объекты, и при изменении их содержимого происходит перераспределение памяти. Это вызовет исключение, если перераспределение памяти происходит в другом модуле. Поэтому следует использовать переменные типа PChar для обмена текстовой информации с DLL.

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

procedure SendString(P:pchar); stdcall; external 'FirstLib.dll' name 'SendString'; 

  

procedure TForm1.Button5Click(Sender: TObject); 

var 

  S:string; 

begin 

  S:='This test string will be sended to DLL'; 

  SendString(pchar(S)); 

end; 

  

Метод SendString в DLL реализован следующим образом

  

procedure SendString(P:pchar); stdcall; export; 

var 

  S:string; 

begin 

  S:=StrPas(P); 

  ShowMessage(S); 

end; 

При запуске данного примера появится сообщение, которое показывает содержимое строки, созданной в *.exe-модуле. Для того чтобы получить текстовую информацию из DLL, в приложении обычно создается буфер, который заполняется в DLL. Естественно, размер буфера должен быть достаточным для хранения всей текстовой информации. Чтобы обезопасить буфер от переполнения, вместе с буфером в качестве параметра чаще всего посылается его размер. Типичный пример получения текстовой информации из DLL выглядит следующим образом:

procedure ReceiveString(P:pchar; Size:integer); stdcall;

   external 'FirstLib.dll' name 'ReceiveString';

  

procedure TForm1.Button6Click(Sender: TObject); 

var 

  C:array[0..1000] of char; 

  S:string; 

begin 

  ReceiveString(C,1000); 

  S:=StrPas(C); 

  Caption:=S; 

end; 

Метод ReceiveString реализован в DLL:

procedure ReceiveString(P:pchar; Size:integer); stdcall; export;

var 

  S:string; 

  N:integer; 

begin 

  FillMemory(P,Size,0); 

  if InputQuery('The string will be transferred to application','Type text',S) then begin 

    N:=length(S); 

    if N>Size then N:=Size; 

    if N>0 then System.Move(S[1],P[0],N); 

  end; 

end; 

Именно таким образом работает большинство методов Windows API, предоставляющих в приложение текстовую информацию.

Можно использовать и переменную типа PChar для получения указателя из DLL, но в этом случае в DLL должна быть глобальная переменная, куда помещают текстовую информацию:

procedure ReceiveBuffer(var P:pchar); stdcall; external 'FirstLib.dll' name 'ReceiveBuffer';

  

procedure TForm1.Button7Click(Sender: TObject); 

var 

  P:pchar; 

  S:string; 

begin 

  ReceiveBuffer(P); 

  S:=StrPas(P); 

  Caption:=S; 

end; 

В DLL данный метод реализован так:

var {The variable can not be local!}

  Buffer:array[0..1000] of char; 

  

procedure ReceiveBuffer(var P:pchar); stdcall; export; 

var 

  S:string; 

  N:integer; 

begin 

  FillMemory(@Buffer[0],1001,0); 

  if InputQuery('The string will be transferred to application','Type text',S) then begin 

    N:=length(S); 

    if N>1000 then N:=1000; 

    if N>0 then System.Move(S[1],Buffer[0],N); 

  end; 

  P:=@Buffer[0]; 

end; 

Буфер нельзя определять как локальную переменную — после отработки метода ReceiveBuffer в DLL тот стек, куда помещаются локальные переменные, будет разрушен и возвращаемый указатель будет указывать на недоступную область памяти.

Аналогично, с использованием буфера, можно передавать любые двоичные данные между приложением и DLL. Но если размер двоичных данных варьируется в широких пределах, могут возникнуть проблемы, связанные с размером буфера. Например, размер OLE-документов может быть от нескольких десятков байт до нескольких десятков мегабайт. При использовании описанной выше технологии размер буфера должен быть равным максимально возможному размеру данных. Но если объявить буфер размером, скажем, 50 Мбайт, то большинство современных компьютеров начнет создавать временное хранилище на диске. При этом передача даже небольших документов будет занимать заметное время.

Выход из данной ситуации состоит в резервировании памяти для хранения объекта в DLL и освобождении системных ресурсов в приложении, но без использования диспетчеров памяти приложения и DLL! Пример приведен ниже:

procedure ReceiveWinAPI(var HMem:integer); stdcall;

   external 'FirstLib.dll' name 'ReceiveWinAPI';

  

procedure TForm1.Button8Click(Sender: TObject); 

var 

  H:integer; 

  P:pchar; 

  S:string; 

begin 

  S:=''; 

  ReceiveWinAPI(H); 

  if H>0 then begin 

    P:=GlobalLock(H); 

    S:=StrPas(P); 

    GlobalUnlock(H); 

    GlobalFree(H); 

  end; 

  Caption:=S; 

end; 

Реализация в DLL метода ReceiveWinAPI:

procedure ReceiveWinAPI(var HMem:integer); stdcall; export;

var 

  S:string; 

  N:integer; 

  P:pchar; 

begin 

  HMem:=0; 

  if InputQuery('The string will be transferred to application','Type text',S) then begin 

    N:=length(S); 

    if N>0 then begin 

      HMem:=GlobalAlloc(GMEM_DDESHARE or GMEM_MOVEABLE or

               GMEM_ZEROINIT,N+1); 

      if HMem>0 then begin 

        P:=GlobalLock(HMem); 

        if Assigned(P) then System.Move(S[1],P[0],N); 

      end; 

    end; 

  end; 

end; 

Здесь память резервируется в DLL, а освобождается в приложении. Для резервирования и освобождения памяти используются методы Windows API GlobalAlloc и GlobalFree соответственно. Памяти резервируется ровно столько, сколько необходимо для хранения объекта. Для приведенного выше примера с OLE-документами это означает, что в большинстве случаев не будет происходить обращение к виртуальной памяти на диске и, следовательно, обмен данными будет совершаться быстро.

НОВЫЕ ОТВЕТЫ

Ответов нет:( Всему виной Format C:

НОВЫЕ ВОПРОСЫ

Вопрос #1 от Voronin N.A.

Здравствуйте
Имеется вопрос.
Я использую компонент ADOTable для обращения к базе Access 2000.
В таблице базы имеется логическое поле.
Мне необходимо чтобы в DBgrid, в этом поле данные отображались не как
true/false, а как Да/Нет.
Кажеться, должно быть стандартное свойство, но я не могу найти.
Не хочется использовать событие DrawCell.

Кто знает пожалуйста напишите vornik@mail.kz

ОТВЕТИТЬ


Вопрос #2 от Борис Машенцов

Здравствуйте, помогите, пожалуйста, создать и запустить собственную службу в Windows2000/XP.
Заранее благодарен!

ОТВЕТИТЬ


Вопрос #3 от Шут

Что-нибудь про TcpClient и TcpServer
киньте на мыло. Плиззз!!!

ОТВЕТИТЬ


Вопрос #4 от mOOr

Здравствуйте!
У меня такой вопрос по Delphi:
на компе два устройства вывода звука( PCI звуковуха и интегрированная с
материнской платой).
Windows использует PCI'шную по умолчанию.
Как из своей программы я могу менять устройство вывода звука по умолчанию
для всей системы?
Если Вы найдёте время, ответ шлите на e-mail:
moor@home.tula.net

ОТВЕТИТЬ


Вопрос #5 от SINNER

Доброе время суток!
У меня токой вопрос по Delphi.
Как сделать чтоб из выбранной директории можно было поменять название группы файлов
начиная допустим с 1 и закачивая n-числом или от 'a' до 'z...', и поменять, на указанное мной, расшерение.
Исли можно исходничёк с коментариями, пожалуйста.
Заранее спасибо.

ОТВЕТИТЬ


Ведущий рассылки:  Angel       © GoldFaq.ru Team     14 - ый выпуск



http://subscribe.ru/
E-mail: ask@subscribe.ru
Отписаться
Убрать рекламу


В избранное