Export — Директива Delphi


Содержание

Delphi и COM — Елманова Н.

°б’ьекты. В секции exports COM DLL содержатся четыре которых
будет сказано далее. Конечно, можно добавить и другие методы,

но для их вызова не потребуется COM-технология. Для экспорта других дов COM DLL
используют интерфейсы. Все методы (как объявленные в с МвТо’ exports, так и
экспонируемые через интерфейсы) обязаны иметь директивь И зова либо stdcall, либо
safecall. Эти директивы будут обсуждаться ниже ВЫ’
Статическая и динамическая загрузка DLL
Модуль может вызывать методы другого модуля, тот, в свою очередь, мет следующего,
и т. д. Например приложение вызывает DLL, а эта DLL вызьша 1 методы другой DLL —
так можно формировать длинные цепочки вызовов дЛя вызова метода из другого
модуля необходимо сначала загрузить его в память а затем определить адрес метода.
Существует два способа загрузки и определени i адреса метода — статический и
динамический.
При статической загрузке для вызова другого модуля следует в какой-либо из
секций описать метод, вызываемый из DLL, одним из следующих способов-
function AddlCK:integer):integer: stdcall: external ’FirstLib.dll’ name ‘CalculateSum’: function
Addl(K:integer):integer; stdcall: external ’FirstLib.dll’ index 1;
Для тестирования необходимо описать в приложении внешний метод одним из
вышеупомянутых способов и создать, например, обработчик события OnClick кнопки,
помещенной на форму вместе с компонентом TEdit:
procedure TForml.ButtonlClick(Sender: TObject): var
N: integer: begi n
N := StrToInt(Editl.Text);
N := Addl(N);
Editl.Text := IntToStr(N); end;
При щелчке на кнопке будет вызываться метод из DLL. Обратите внимание на
изменение имени метода; из обработчика события OnCl i ck вызывается метод с
именем Addl. Этот метод экспонируется в DLL под именем Calcul ateSum. В реали-
зации DLL он имеет название AddOne.
При таком определении метода DLL будет загружена немедленно после старта
приложения и выгружена вместе с его завершением. В приведенном выше примере следует
обратить внимание на то, что после имени динамически загр> жаемой библиотеки указано ее
расширение (FirstLib.dll). Такая конструкция не обходима для загрузки библиотеки в Windows
NT — без расширения *.dll Фа • не будет найден. Для работы приложения в Windows 95
указывать расшире не обязательно. ннаЯ
При поиске DLL для загрузки первоначально определяется, была ли да библиотека
уже загружена в память другим модулем. Если была — извлек адрес вызываемого метода
и затем этот адрес передается приложению, с j ^ нет — система начинает ее поиск на
диске. При этом если в имени DLL n’^er0. указан в явном виде, то система ищет
библиотеку в каталоге модуля, nbITa*DovVS ся загрузить DLL. Если не находит — то
продолжает поиски в каталогах W

p0WS\SYSTEM (или WIN NT, WINNT\SYSTEM, WINNT\SYSTEM32). После это- l* оисходит
поиск Б каталогах, определенных в переменной среды Path. Если Г° Г йоте ка с заданным
именем будет найдена, то она загрузится и приложение ТУет. Если же нет — то
происходит исключение и приложение прекращает СТ ю работу. Приложение также
прекращает свою работу, если не найден метод СВ° иным именем (или индексом, если
он импортируется по индексу).
Предыдущая 114 115 116 117 118 119 .. 253 >> Следующая

Export — Директива Delphi

Динамически загружаемые библиотеки (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:

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

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

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

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

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

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

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

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

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

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

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

При нажатии кнопки будет вызываться метод из 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 выглядит следующим образом:

Первоначально определяется новый процедурный тип, например 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 выглядит следующим образом:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Вызов методов приложения в DLL

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

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

В приложении создается метод (не метод класса!), адрес которого передается в DLL. Для того чтобы данный метод можно было вызывать из DLL, созданных на других языках программирования, желательно использовать соглашение stdcall вызова при реализации указанных методов (так называемые callback-методы). Использование вызова метода в DLL можно проиллюстрировать на таком примере:

При необходимости вызвать метод объекта следует учитывать, что данный метод объекта характеризуется двумя адресами — адресом метода и адресом данных. Следовательно, необходимо передавать два указателя. Но вместо передачи двух указателей можно воспользоваться структурой TMethod, определенной в модуле SysUtils.pas:

Код в приложении для приведенного выше примера выглядит так:

Код в DLL, осуществляющий вызов метода объекта, выглядит следующим образом:

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

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

Обратите внимание: теперь в приложении (в проекте, где будет генерироваться *.exe-файл[U2] [NE3] ) определена секция exports! Соответствующий код в DLL для тестирования данного метода выглядит следующим образом:

При запуске этого проекта приложение автоматически загружает DLL и находит адрес метода в DLL CalculateNextExport (который экспортируется по имени SumExport). Загруженная DLL в качестве параметров получает заголовок модуля приложения и имя метода, под которым [U4] он экспортируется в приложении. Далее DLL использует метод GetProcAddress для нахождения адреса метода, экспортируемого приложением. Для того чтобы был возвращен адрес метода, в приложении объявляется секция exports, где описано внешнее имя метода. Этот пример показывает формально одинаковые структуру и способ формирования *.exe- и *.dll-файлов.

Модальные формы в DLL

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

Данный код вызова диалога следует применять только при статической загрузке DLL. При выполнении диалога в DLL нужно учитывать, что формы в DLL не могут создаваться вместе с запуском DLL, как это возможно в приложении при установке опции Auto-Create Form в разделе Project/Options/Forms. Поэтому форму необходимо создавать динамически, вызывая ее конструктор Create из кода. Соответственно перед выходом из процедуры, вызывающей форму, необходимо вызвать ее деструктор (в нашем примере — FDialog.Release). Необходимо учитывать, что в DLL создается объект типа TApplication. Поскольку и само приложение имеет данный объект, то (если не принимать никаких мер) на экране в панели приложений появятся две пиктограммы: одна — для приложения, другая — для DLL, выполняющей диалог (рис. 2).

При этом после нажатия на пиктограмму приложения оно активируется и всплывает главная форма, но доступ к элементам управления главной формы получить нельзя. Ясно, что такое поведение является некорректным. Поэтому в качестве параметра метода в DLL, выполняющего диалог, необходимо использовать ссылку на объект TApplication (точнее, его свойство Handle) приложения. Посредством присвоения Application.Handle:=AppHandle; в DLL уничтожается объект TApplication и приложение начинает поддержку рассылки сообщений элементам управления, созданным в DLL. В этом случае на панели задач присутствует одна пиктограмма приложения — это корректно. Приведем типичный пример кода основного приложения, вызывающего диалог из DLL:

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

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

В этом случае библиотека выгружается немедленно после выполнения диалога, а метод Release, который рекомендуется использовать для вызова деструктора формы, посылает сообщения CM_RELEASE самой форме посредством вызова метода PostMessage. Этот метод ставит сообщение в конец очереди, приложение продолжает выполнять код — и выгружает DLL! Только после выполнения кода начинается обработка очереди сообщений, в конце концов достается сообщение CM_RELEASE, делается попытка выполнить деструктор формы — но методы-то уже выгружены! Если операционная система обладает значительным резервом ресурсов, то велика вероятность, что место в ОЗУ, где хранился код, сохранит свое содержимое и форма диалога будет разрушена успешно. Но при небольшом количестве ресурсов на освободившееся место в ОЗУ немедленно помещаются какие-либо данные из виртуальной дисковой памяти, а попытка выполнить деструктор кончается исключением. Поэтому при динамической загрузке обязательно нужно использовать метод Free вместо Release. Кроме того, перед выгрузкой DLL рекомендуется вызвать метод Application.ProcessMessages.

Вторая проблема состоит в следующем. Если использовать один и тот же объект TApplication в приложении и DLL посредством выполнения приведенного выше оператора — Application.Handle:=AppHandle; — перед вызовом метода ShowModal в DLL, то после выгрузки DLL главная форма приложений минимизируется и ее необходимо восстанавливать вновь. Один из способов решения проблемы — вызвать метод ShowWindow(Handle,SW_RESTORE) сразу же после выполнения команды FreeLibrary в приложении. Однако при этом главная форма приложения будет мерцать. Другой способ — оставить разные объекты TApplication в приложении и DLL,— для этого не надо выполнять оператор Application.Handle:=AppHandle; в DLL. Но тогда на панели задач появляются две пиктограммы. Корректное решение проблемы — присвоить нулевое значение свойству Application.Handle в DLL перед выходом из метода. Ниже приведен рекомендуемый код в DLL, которая выполняет диалог при динамической загрузке:

Приведем код приложения, динамически вызывающего данную DLL:

Немодальные формы в DLL

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

Как и при показе модальных форм, необходимо присвоить дескриптор окна главного приложения приложению, создаваемому в DLL, — иначе на панели задач будут показаны две иконки. Затем просто создается форма и вызывается ее метод Show. Такой способ показа немодальных форм приводит к тому, что из главного приложения можно неоднократно вызвать данный метод и тем самым создать несколько экземпляров форм. Зачастую это оправданно, поскольку одинаковые типы форм могут содержать, например, разные документы. Но при этом способе показа рекомендуется сделать обработчик события OnClose для TForm1 и присвоить параметру CloseAction значение caFree — в противном случае при закрытии форма будет спрятана [NE5][U6] на экране без освобождения системных ресурсов.

Илон Маск рекомендует:  Что такое код gmp_popcount

Для показа единственного экземпляра немодальной формы следует немного изменить код:

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

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

Иногда возникает необходимость показа немодальных форм из динамически загружаемых DLL, например при редком использовании в приложении немодальных форм для экономии ресурсов. Если реализовать код так же, как и при показе модальных диалогов, то форма будет создана и, может быть, даже показана на экране. Но после этого произойдет выгрузка DLL, а затем немедленно последуют исключения, поскольку в памяти компьютера будет отсутствовать код для работы с элементами управления формы. Традиционное решение этой проблемы выглядит следующим образом: загружается динамическая библиотека, в качестве одного из параметров передается адрес метода главного приложения, который будет вызван при закрытии немодальной формы — обычно в обработчике события OnDestroy. Этот метод должен информировать главное приложение о необходимости выгрузки DLL из памяти компьютера, но DLL должна выгружаться после завершения его работы (и, следовательно, после завершения работы деструктора формы) — иначе возможно исключение из-за отсутствия кода в памяти компьютера. Выгрузка DLL после завершения работы приложения [U7] достигается с использованием асинхронной развязки — посылки сообщения методом PostMessage какому-либо окну приложения, обычно главной форме. Приведем код реализации данной технологии в DLL:

Приложение, использующее эту DLL, имеет следующий код (WM_DLLUNLOAD определена как константа в секции interface модуля):

Очевидно, что код получается довольно громоздким: в главном приложении необходимо реализовывать три метода вместо одного. Альтернативный вариант можно предложить исходя из того, что в DLL имеется объект TApplication, который может поддерживать цикл выборки сообщений. Но в DLL нельзя создать форму, используя метод TApplication. CreateForm, так как соответствующая закладка диалога Project/Options/Forms отсутствует в проектах Delphi 4 и 5 и неактивна в Delphi 3. Однако можно вызвать все методы объекта Tapplication, вручную дописав соответствующий код в DLL:

Следует обратить внимание, что дескриптор главного приложения не присваивается в данном проекте дескриптору TApplication в DLL. Это реально приводит к появлению двух пиктограмм на панели приложений. Правда, в некоторых случаях это полезно — так легче добраться до перекрытых окон. Интересно отметить, что в Delphi 3 после написания данного кода становятся доступными элементы управления диалога Project/Options/Forms, где можно определить автоматически создаваемые формы и главную форму приложения. Код главного приложения, использующий данную DLL, таков:

В отличие от предыдущего примера, динамическая загрузка DLL и ее выгрузка осуществляются в одном методе, да и объем написанного кода существенно меньше.

Экспорт дочерних форм из DLL

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

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

На форму TForm1 помещают элементы управления, которые необходимо показать в главном приложении. Обычно свойство BorderStyle дочерних форм устанавливают равным bsNone (рис. 3).

Далее, дочерним формам в качестве параметров метода следует передавать прямоугольную область родительского окна, в которой она размещается. Это означает, что дочерняя форма должна иметь хорошо отлаженные обработчики событий, которые связаны с изменением ее размеров. Как и во всех предыдущих примерах, показ дочерних форм следует сделать независимым от языка; следовательно, необходимо использовать методы Windows API для изменения родителей. Такой метод определен в модуле user32.dll — SetParent. Другой метод Windows API — SetWindowPos — используется для изменения границ формы. В качестве возвращаемого параметра возвращается указатель на объект. Но приложение не будет его использовать, поскольку оно должно его запоминать и использовать при вызове метода DeleteCustomWindow. Поэтому сохраняется возможность использовать данный проект в приложениях, написанных не на Delphi, а на других языках.

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

Код основного приложения для тестирования данной DLL выглядит следующим образом:

В этом проекте на форму TForm2 никакие элементы управления не помещаются. Главная форма приложения содержит одну кнопку, при нажатии на которую выполняется немодальный показ TForm2:

with TForm2.Create(Self) do Show;

При создании второй формы происходит загрузка DLL, а форма, созданная в DLL, становится дочерней для TForm2. Можно создать несколько экземпляров TForm2. При разрушении конкретного экземпляра разрушается и дочернее окно на нем, для чего используется сохраненный ранее параметр FchildID (рис. 4).

Казалось бы, аналогичную методику можно использовать для экспорта из DLL других элементов управления, среди предков которых не содержится класс TForm. Однако при попытке использовать метод SetParent непосредственно для элементов управления происходит генерация исключения об отсутствии у элемента управления родительского окна, и этот элемент [U8] не показывается на форме.

Заключение

Итак, с помощью динамически загружаемых библиотек можно оптимизировать ресурсы, необходимые для выполнения приложений; использовать в проектах модули, написанные на различных языках программирования; создавать проекты, которые могут иметь необязательные функции и пункты меню. Вызов методов из DLL не представляет трудностей, за исключением того, что следует обращать особое внимание на исключительные ситуации: не допускать попадания экземпляра — потомка Exception в главный модуль, обязательно вызывать команду FreeLibrary при наличии исключений. Анализу исключительных ситуаций и отладке приложений будет посвящена следующая статья данного цикла.

Export — Директива Delphi

На этом шаге мы рассмотрим. основные элементы DLL .

Delphi имеет мастер для создания DLL , который вызывается командой File | New | Other и последующим выбором значка DLL Wizard на странице New репозитария объектов. При этом возникает заготовка для реализации DLL :

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

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

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

Рис.1. Изменение состояния стека при вызове функций

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

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

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

Поэтому в языках программирования высокого уровня можно объявить, кто будет очищать стек и в какой очередности параметры функции будут помещаться в стек. Такое объявление называется соглашением о вызовах (calling convention) . Имеется ряд зарезервированных слов, которые помещаются после заголовков функций (таблица 1).

Таблица 1. Соглашения о вызовах в Delphi
Директива Порядок следования параметров Очистка стека Регистры
register Слева направо Вызываемая функция Используются
pascal Слева направо Вызываемая функция Не используются
cdecl Справа налево Вызывающий код Не используются
stdcall Справа налево Вызываемая функция Не используются
safecall Справа налево Вызываемая функция Не используются

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

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

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

Для экспонирования функции в секции exports просто приводится ее название (AddOne) , после которого следует либо служебное слово index с целочисленным идентификатором после него (идентификатор должен быть больше нуля), либо служебное слово name с текстовым идентификатором, либо оба вместе, как в данном случае. Внешний модуль может обращаться к конкретной функции как по индексу, так и по имени. Как это делается — будет рассказано в следующих шагах.

На данном этапе изложения материала следует отметить, что название функции AddOne нигде и никогда не будет видно во внешних модулях — будет использоваться либо целочисленная константа 1, либо имя CalculateSum . Сразу же следует отметить, что имя чувствительно к регистру букв — функция не будет найдена, если использовать, например, такие имена, как calcuiatesum или CALCULATESUM . Индексы, если они объявляются в секции exports , обязательно должны начинаться с 1 и принимать все последовательные целочисленные значения (2, 3, 4. ). Нельзя опускать какое-либо число в этой последовательности натуральных чисел, иначе могут возникнуть ошибки при обращении к функции по ее индексу. Эта ошибка проявляется в Windows 95 OSR-2 из-за некорректной реализации этой операционной системы. Для исправления ошибки компания Microsoft объявила о том, что библиотека обязательно должна экспортироваться по имени. Поэтому во вновь создаваемых библиотеках необходимо объявлять имя функции в секции exports , при этом индексы объявлять не следует (или, по крайней мере, не следует использовать их для выяснения адреса функции).

И, наконец, следует рассмотреть отличия в реализации обычных библиотек и библиотек, содержащих СОМ -объекты. В секции exports библиотек, содержащих СОМ -объекты, имеется четыре функции, о которых будет сказано в следующих шагах. Конечно, можно добавлять и другие функции, но для их вызова не требуется СОМ . Для экспорта других функций библиотеки СОМ используют интерфейсы. Все функции COM DLL (как объявленные в секции exports , так и экспонируемые как методы СОМ -интерфейсов) обязательно должны иметь директивы вызова либо stdcall , либо safecall . Эти директивы будут обсуждаться в следующих шагах.

На следующем шаге мы рассмотрим статическую и динамическую загрузку DLL .

Export — Директива Delphi

Создание DLL в Delphi (экспорт)

Использование DLL в Delphi (импорт)

DLL, использующие объекты VCL для работы с данными

Исключительные ситуации в DLL

    1. Понятие DLL
    2. Вспомним процесс программирования в DOS. Преобразование исходного текста программы в машинный код включал в себя два процесса — компиляцию и линковку. В процессе линковки, редактор связей, компоновавший отдельные модули программы, помещал в код программы не только объявления функций и процедур, но и их полный код. Вы готовили таким образом одну программу, другую, третью . И везде код одних и тех же функций помещался в программу полностью (см. рис 1).

    Рис.1 : Вызов функций при использовании статической компоновки

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

    Рис.2: Вызов функций при использовании динамической компоновки

    Но, чем же отличаются Dynamic Link Library (DLL) от обычных приложений? Для понимания этого требуется уточнить понятия задачи (task), экземпляра (копии) приложения (instance) и модуля (module).

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

    Задача 1 Задача 2

    Копия 1 приложения Копия 2 приложения

    Очередь сообщений Очередь сообщений

    Рис.3 : Копии приложения и модуль приложения

    DLL — библиотека также является модулем. Она находится в памяти в единственном экземпляре и содержит сегмент кода и ресурсы, а также сегмент данных (см. рис. 4).

    Рис.4 : Структура DLL в памяти

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

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

    Часто, в виде DLL создаются отдельные наборы функций, объединенные по тем или иным логическим признакам, аналогично тому, как концептуально происходит планирование модулей ( в смысле unit ) в Pascal. Отличие заключается в том, что функции из модулей Pascal компонуются статически — на этапе линковки, а функции из DLL компонуются динамически, то есть в run-time.

    Для программирования DLL Delphi предоставляет ряд ключевых слов и правил синтаксиса. Главное — DLL в Delphi такой же проект как и программа.

    Рассмотрим шаблон DLL:

    Имя файла проекта для такого шаблона должно быть MYDLL.DPR.

    . К сожалению, в IDE Delphi автоматически генерируется только проект программы, поэтому Вам придется проект DLL готовить вручную. В Delphi 2.0 это неудобство устранено.

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

    Экспортирование функций (и процедур ) может производится несколькими способами:

    В зависимости от этого используется различный синтаксис:

    procedure ExportByOrdinal; export;

    ExportByOrdinal index 10;

    procedure ExportByName; export;

    ExportByName name ‘MYEXPORTPROC’;

    Так как в Windows существует понятие «резидентных функций» DLL, то есть тех функций, которые находятся в памяти на протяжении всего времени существования DLL в памяти, в Delphi имеются средства для организации и такого рода экспорта:

    ExportByName name ‘MYEXPORTPROC’ resident;

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

    Если же Вы будете экспортировать функции следующим образом:

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

        1. Использование DLL в Delphi (импорт)
        2. Для организации импорта, т.е. доступа к функциям, экспортируемым из DLL, так же как и для их экспорта, Delphi предоставляет стандартные средства.

        Для показанных выше примеров, в Вашей программе следует объявить функции, импортируемые из DLL таким образом:

        procedure ImportByName;external ‘MYDLL’ name ‘MYEXPORTPROC’;

        procedure ImportByOrdinal; external ‘MYDLL’ index 10;

        procedure MyExportFunc1; external ‘MYDLL’;

        Этот способ называется статическим импортом.

        Как Вы могли заметить, расширение файла, содержащего DLL, не указывается — по умолчанию подразумеваются файлы *.DLL и *.EXE. Как же тогда быть в случае, если файл имеет другое расширение (например, как COMPLIB.DCL в Delphi), или если требуется динамическое определение DLL и импортируемых функций (например, Ваша программа работает с различными графическими форматами, и для каждого из них существует отдельная DLL.)?

        Для решения такого рода проблем Вы можете обратиться напрямую к API Windows, используя, так называемый, динамический импорт:

        if Handle>=32 then < if

        if MyImportProc<>nil then

        . Синтаксические диаграммы объявлений экспорта/импорта, подмена точки выхода из DLL, и другие примеры, Вы можете найти в OnLine Help Delphi, Object Pascal Language Guide, входящему в Borland RAD Pack for Delphi, и, например, в книге «Teach Yourself Delphi i n 21 Days».

        Если не говорить о генерируемом компилятором коде (сейчас он более оптимизирован), то все правила синтаксиса остались те же , что и в Borland Pascal 7.0

      1. DLL, использующие объекты VCL для работы с данными
      2. При создании своей динамической библиотеки Вы можете использовать вызовы функций из других DLL. Пример такой DLL есть в поставке Delphi (X:\DELPHI\DEMOS\BD\BDEDLL). В эту DLL помещена форма, отображающая данные из таблицы и использующая для доступа к ней объекты VCL (TTable, TDBGrid, TSession), которые, в свою очередь, вызывают функции BDE. Как следует из комментариев к этому примеру, для такой DLL имеется ограничение: ее не могут одновременно использовать несколько задач. Это вызвано тем, что объект Session, который создается автоматически при подключении модуля DB, инициализируется для модуля, а не для задачи. Если попытаться загрузить эту DLL вторично из другого приложения, то возникнет ошибка. Для предотвращения одновременной загрузки DLL несколькими задачами нужно осуществить некоторые действия. В примере — это процедура проверки того, используется ли DLL в данный момент другой задачей.
      3. Исключительные ситуации в DLL

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

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

Оглавление

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

В главе 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 :

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

DLL экспорт строк

известно что dll может работать со строками только ввиде PAnsiChar и PWideChar
Данные типы достаточно легко трансформируются в Delphi String если бы не одно НО. Их нужно гдето уничтожать, ибо выделенная память под указатель строки PAnsiChar никуда не девается после трансформации

в соседней теме работа с указателями Pchar
разобран вопрос о структуре и выделении памяти под строки

исходный код DLL демонструющий получение данных через PAnsiChar

так как строка в delphi уже из себя представляет Pchar,
то достаточно отправить на экспорт указатель на 1-й символ
никакого дублирования памяти не происходит

при уничтожении delphi автоматически, средствами своего же языка уничтожит строку лежащую в записи privateStruct
указатель в publicStruct не требует чистки так как ссылается на delphi строку

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

Вложения

testHelloWordDLL.zip (1.63 Мб, 9 просмотров)
03.11.2013, 14:34

Экспорт dll (из с++) в с#
Подскажите, как нужно декларировать функционал в с++ коде (в dll) чтобы я её в C# коде мог вызвать.

Экспорт Delphi dll in C#
Есть dll в которой описан класс на делфи: type TSMS_Sender = class(TComponent) private .

Экспорт функции из dll на с++
Здравствуйте, помогите пожалуйста. Не могу экспортировать функцию LibreryNIFound() из dll. dll.

Экспорт классов из DLL
Привет. Вот так я экспортирую обычные ф-ии: main.h #ifndef __MAIN_H__ #define __MAIN_H__ .

Экспорт функции DLL
Вечер добрый. Работаю над проектом, в котором из DLL экспортируются функции для другого проекта.

03.11.2013, 21:30 2

Здесь сказано следующее. Что если DLL будет обмениваться с вызывающей программой динамическими данными, то в проекте DLL и в проекте вызывающей программы надо подключить менеджер памяти ShareMem, который расположен в библиотеке BORLNDMM.DLL. Для этого надо в проекте вызывающей программы в файле *.DPR в самое начало списка USES добавить модуль ShareMem. И в проект DLL в список USES, тоже в самое начало добавить этот же модуль: ShareMem. При этом, надо учесть, что если программа будет скопирована на другие компьютеры, то вместе с ней следует поставлять библиотеку BORLNDMM.DLL.

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

Export — Директива Delphi

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

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

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

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

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

Применение динамических библиотек позволяет добиться ряда преимуществ:

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

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

В этой главе рассматриваются следующие вопросы:

  • структура файла DLL;
  • инициализация DLL;
  • явная и неявная загрузка;
  • вызовы функций из динамической библиотеки;
  • ресурсы в динамических библиотеках.


Для создания динамической библиотеки в Репозитории Delphi имеется специальный шаблон. Его значок DLL Wizard расположен на странице New Репозитория. В отличие от проекта обычного приложения, проект DLL состоит всего из одного исходного файла. Впоследствии к нему можно добавлять отдельные модули и формы.

Листинг 28.1. Исходный файл проекта динамической библиотеки

Обширный комментарий в каждом проекте DLL касается использования модуля ShareMem . О нем рассказывается ниже.

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

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

Блок begin..end называется блоком инициализации библиотеки и предназначен для размещения кода, который автоматически выполняется при загрузке DLL.

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

При создании динамических библиотек очень удобно использовать группы проектов. В группу помещается проект приложения и проект (проекты) необходимой для его работы динамической библиотеки (библиотек). Для переключения между проектами удобно использовать Диспетчер проектов (команда Project Manager из меню View ). Его можно поместить в окно Редактора кода.

Еще один способ удобной работы с проектами динамических библиотек заключается в задании для DLL вызывающей программы. Это делается в диалоге команды Parameters из меню Run (рис. 28.1). Вызывающее приложение задается в группе Host Application. В результате после компиляции динамической библиотеки вызывается использующее ее приложение.

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

Рис. 28.1. Диалог команды Parameters меню Run

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

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

Листинг 28.2. Исходный код динамической библиотеки DataCheck

Windows, SysUtils, Classes, Messages, Forms,

Dialogs, StdCtrls, ComCtrls;

function ValidDate(AText: String): Integer;

Result := 0; StrToDate(AText);

on E:EConvertError do Result := -1;

function ValidTime(AText: String): Integer;

Result := 0; StrToTime(AText);

on E:EConvertError do Result := -1;

function Validlnt(AText: String): Integer;

Result := 0; StrToInt(AText);

on E:EConvertError do Result := -1;

ValidDate index 1, ValidTime index 2 name ‘IsValidTime’;

then ShowMessage(‘Год представлен двумя цифрами’);

Итак, три функции этой библиотеки обеспечивают проверку строки перед преобразованием ее в целое число, дату или время. Для обеспечения экспорта этих функций их необходимо объявить в секции exports .

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

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

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

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

В первом варианте компилятор самостоятельно определяет положение функции в таблице экспорта.

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

Ключевое слово name позволяет экспортировать функцию под другим именем.

Соглашения о вызовах

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

Стандартный вызов в языках C++ и Object Pascal различается, но набор директив смены типа вызова позволяет обеспечить любую реализацию.

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

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

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

Помимо рассмотренных ниже директив имеются еще три типа вызовов, которые не используются и сохранены для обеспечения обратной совместимости. Это директивы near , far, export.

Директива register

Эта директива используется по умолчанию. Поэтому нет необходимости добавлять ключевое слов register после объявления функции. Вызов такого типа называется быстрым (fast call). В нем используются три расширенных регистра процессора, в которые помещаются переменные длиной не более 32-х разрядов и указатели. Остальные параметры помещаются в стек слева направо. После использования стек очищается вызываемой процедурой.

Директива pascal

Реализует вызовы в стиле языка Pascal . За очистку стека отвечает вызываемая процедура. Параметры помещаются в стек слева направо. Этот способ вызова является очень быстрым, но не поддерживает переменное число параметров. Используется для обеспечения обратной совместимости.

Директива stdcall

Параметры помещаются в стек слева направо. Очистка стека осуществляется вызываемой процедурой. Этот вызов обеспечивает обработку фиксированного числа параметров.

Директива cdecl

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

Эта директива в основном применяется для обращения к динамическим библиотекам, использующим соглашения о вызовах в стиле языка С. Использование директивы cdecl для библиотек Delphi не вызовет ошибку компиляции, но переменное число параметров не обеспечит.

Директива safecall

Параметры помещаются в стек справа налево. Очистка стека осуществляется вызываемой процедурой. Используется в СОМ и основанных на ней технологиях.

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

При загрузке динамической библиотеки выполняется код инициализации, который расположен в блоке begin, .end (см. листинги 28.1 и 28.2). Обычно здесь выполняются операции по заданию начальных значений используемых в функциях библиотеки переменных, проверка условий функционирования DLL, создание необходимых структур и объектов и т. д.

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

Любые объявленные в DLL глобальные переменные недоступны за ее пределами.

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

Итак, перед запуском кода инициализации автоматически вызывается встроенная ассемблерная процедура _initDLL (она расположена в модуле system ). Она сохраняет состояние регистров процессора; получает значение экземпляра модуля библиотеки и записывает его в глобальную переменную hinstance ; устанавливает для глобальной переменой isLibrary значение True (по этому значению вы всегда сможете распознать код DLL); получает из стека ряд параметров; проверяет переменную процедурного типа DLLProc :

Эта переменная используется для проверки вызовов операционной системой точки входа DLL . С этой переменной можно связать процедуру с одним целочисленным параметром. Такая процедура называется функцией обратного вызова системного уровня.

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

  • Значение DLL_PROCESS_DETACH передается при выгрузке DLL из адресного пространства процесса. Это происходит при явном вызове системной функции FreeLibrary (см. ниже) или при завершении процесса.
  • Значение DLL_PROCESS_ATTACH означает, что библиотека отображается в адресное пространство процесса, который загружает ее в первый раз.
  • Значение DLL_THREAD_ATTACH посылается всем загруженным в процесс динамическим библиотекам при создании нового потока. Обратите внимание, что при создании процесса и первичного потока посылается только одно значение DLL_PROCESS_ATTACH.
  • Значение DLL_THREAD_DETACH посылается всем загруженным в процесс динамическим библиотекам при уничтожении существующего потока.

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

Это хороший способ организовать в динамической библиотеке необходимую в каждом случае обработку. Как это сделать?

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

Во-вторых, в секции инициализации нужно связать переменную DLLProc и созданную процедуру.

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

Листинг 28.3. Часть исходного кода динамической библиотеки DataCheck c функцией обратного вызова

IsValidDate index 1,

IsValidTime index 2 name ‘ValidTime’,

procedure DLLEntryPoint(Reason: Integer);

DLL_PROCESS_ATTACH: ShowMessage(‘Первая загрузка DLL’); DLL_PROCESS_DETACH:;

DLL_THREAD_ATTACH: ShowMessage(‘Создан новый поток’); DLL_THREAD_DETACH:; end; end;

Процедура DLLEntryPoint обеспечивает простой показ сообщения о полученном значении параметра. В коде инициализации глобальной переменной DLLProc передается адрес процедуры DLLEntryPoint . Затем эта процедура вызывается явно с параметром DLL_PROCESS_ATTACH .

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

Листинг 28.4. Создание и удаление объекта при загрузке и выгрузке динамической библиотеки DataCheck .

(Часть исходного кода опущена (см. листинг 24.2)>

IsValidDate index 1,

IsValidTime index 2 name ‘ValidTime’,

type TSomeObject = class(TObject)

Fieldl: String; end; var FirstObj: TSomeObject;

procedure DLLEntryPoint(Reason: Word);

case Reason of DLL_PROCESS_ATTACH:

FirstObj := TSomeObject.Create; FirstObj.Fieldl := ‘Объект создан’; ShowMessage(FirstObj.Fieldl);

DLL__PROCESS_DETACH: FirstObj . Free;

DLL_THREAD_ATTACH: ShowMessage(‘Создан новый поток’); DLL_THREAD_DETACH:;

При завершении работы динамической библиотеки вызывается процедура, на которую указывает адрес, содержащийся в переменной ExitProc :

Теперь рассмотрим, как из динамических библиотек вызываются функции.

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

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

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

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

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

Неявный вызов

Механизм неявного вызова наиболее прост, т. к. выполняется автоматически и основан на имеющейся в приложении информации о вызываемых функциях и динамических библиотеках. Однако разработчик не имеет возможности влиять на ход загрузки DLL. Если операционная система не смогла загрузить библиотеку, просто выдается сообщение об ошибке. Единственный способ повлиять на процесс загрузки — использовать секцию инициализации библиотеки (см. выше).

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

Проекты DemoDLL1 и DataCheck объединены в одну группу. Переключение между проектами легко выполняется утилитой Диспетчер проектов.

Листинг 28.5.Модуль главной формы проекта DemoDLL1

Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, comctrls, Buttons;

Export — Директива Delphi

Профиль
Группа: Участник Клуба
Сообщений: 3497
Регистрация: 31.3.2002
Где: Лес

Репутация: 14
Всего: 115

Профиль
Группа: Участник
Сообщений: 291
Регистрация: 17.3.2003

Репутация: нет
Всего: 4

директивы условной компиляции
<$C+>и <$C->— директивы проверки утверждений
<$I+>и <$I->— директивы контроля ввода/вывода
<$M>и <$S>— директивы, определяющие размер стека

<$M+>и <$M->— директивы информации времени выполнения о типах
<$Q+>и <$Q->— директивы проверки переполнения целочисленных операций

<$R>— директива связывания ресурсов

<$R+>и <$R->— директивы проверки диапазона

<$APPTYPE CONSOLE>— директива создания консольного приложения

1) Директивы компилятора, разрешающие или запрещающие проверку утверждений.

По умолчанию <$C+>или

Область действия локальная

Директивы компилятора $C разрешают или запрещают проверку утверждений. Они влияют на работу процедуры Assert,используемой при отладке программ. По умолчанию действует
директива <$C+>и процедура Assert генерирует исключение EAssertionFailed, если проверяемое утверждение ложно.

Так как эти проверки используются только в процессе отладки программы, то перед ее окончательной компиляцией следует указать директиву <$C->. При этом работа процедур Assert будет блокировано и генерация исключений EassertionFailed производиться не будет.

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

2) Директивы компилятора, включающие и выключающие контроль файлового ввода-вывода.

По умолчанию <$I+>или

Область действия локальная

Директивы компилятора $I включают или выключают автоматический контроль результата вызова процедур ввода-вывода Object Pascal. Если действует директива <$I+>, то при возвращении процедурой ввода-вывода ненулевого значения генерируется
исключение EInOutError и в его свойство errorcode заносится код ошибки. Таким образом, при действующей директиве <$I+>операции ввода-вывода располагаются в блоке try. except, имеющем обработчик исключения EInOutError. Если такого блока нет, то обработка производится методом TApplication.HandleException.

Если действует директива <$I->, то исключение не генерируется. В этом случае проверить, была ли ошибка, или ее не было, можно, обратившись к функции IOResult. Эта функция очищает ошибку и возвращает ее код, который затем можно анализировать. Типичное применение директивы <$I->и функции IOResult демонстрирует следующий пример:

В этом примере на время открытия файла отключается проверка ошибок ввода вывода, затем она опять включается, переменной i присваивается значение, возвращаемое функцией IOResult и, если это значение не равно нулю (есть ошибка), то предпринимаются какие-то действия в зависимости от кода ошибки. Подобный стиль программирования был типичен до введения в Object Pascal механизма обработки исключений. Однако сейчас, по-видимому, подобный стиль устарел и применение директив $I потеряло былое значение.

3) Директивы компилятора, определяющие размер стека

Область действия глобальная

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

Если во время работы выясняется, что минимального размера стека не хватает, то размер увеличивается на 4 K, но не более, чем до установленного директивой максимального размера. Если увеличение размера стека невозможно из-за нехватки памяти или из-за достижения его максимальной величины, генерируется исключение EStackOverflow. Минимальный размер стека по умолчанию равен 16384 (16K). Этот размер может изменяться параметром minstacksize
директивы <$M>или параметром number директивы <$MINSTACKSIZE>.

Максимальный размер стека по умолчанию равен 1,048,576 (1M). Этот размер может изменяться параметром maxstacksize директивы <$M>или параметром number директивы <$MAXSTACKSIZE number>. Значение минимального размера стека может задаваться целым числом в диапазоне между1024 и 2147483647. Значение максимального размера стека должно быть не менее минимального размера и не более 2147483647. Директивы задания размера стека могут включаться только в программу и не должны использоваться в библиотеках и модулях.

В Delphi 1 имеется процедура компилятора <$S>, осуществляющая переключение контроля переполнения стека. Теперь этот процесс полностью автоматизирован и директива <$S>оставлена только для обратной совместимости.

4) Директивы компилятора, включающие и выключающие генерацию информации времени выполнения о типах (runtime type information — RTTI).

По умолчанию <$M->или

Область действия локальная

Директивы компилятора $I включают или выключают генерацию информации времени выполнения о типах (runtime type information — RTTI). Если класс объявляется в состоянии <$M+>или является производным от класса объявленного в этом состоянии, то компилятор генерирует RTTI о его полях, методах и свойствах, объявленных в разделе published. В противном
случае раздел published в классе не допускается. Класс TPersistent, являющийся предшественником большинства классов Delphi и все классов компонентов, объявлен в модуле Classes в состоянии <$M+>. Так что для всех классов, производных от него, заботиться о директиве <$M+>не приходится.

5) Директивы компилятора, включающие и выключающие проверку переполнения при целочисленных операциях

По умолчанию <$Q->или

Область действия локальная

Директивы компилятора $Q включают или выключают проверку переполнения при целочисленных операциях. Под переполнением понимается получение результата, который не может сохраняться в регистре компьютера. При включенной директиве <$Q+>проверяется переполнение при целочисленных операциях +, -, *, Abs, Sqr, Succ, Pred, Inc и Dec. После каждой из этих операций размещается код, осуществляющий соответствующую проверку. Если обнаружено переполнение,
то генерируется исключение EIntOverflow. Если это исключение не может быть обработано, выполнение программы завершается.

Директивы $Q проверяют только результат арифметических операций. Обычно они используются совместно с директивами <$R>, проверяющими диапазон значений при присваивании.
Директива <$Q+>замедляет выполнение программы и увеличивает ее размер. Поэтому обычно она используется только во время отладки программы. Однако, надо отдавать себе отчет, что отключение этой директивы приведет к появлению ошибочных результатов расчета в случаях, если переполнение действительно произойдет во время выполнении программы. Причем сообщений о подобных ошибках не будет.

6) Директива компилятора, связывающая с выполняемым модулем файлы ресурсов

Область действия локальная

Директива компилятора <$R>указывает файлы ресурсов (.DFM, .RES), которые должны быть включены в выполняемый модуль или в библиотеку. Указанный файл должен быть файлом ресурсов Windows. По умолчанию расширение файлов ресурсов — .RES. В процессе компоновки компилированной программы или библиотеки файлы, указанные в директивах <$R>, копируются в
выполняемый модуль. Компоновщик Delphi ищет эти файлы сначала в том каталоге, в котором расположен модуль, содержащий директиву <$R>, а затем в каталогах, указанных при выполнении команды главного меню Project | Options на странице Directories/Conditionals диалогового окна в опции Search path или в опции /R командной строки DCC32.

При генерации кода модуля, содержащего форму, Delphi автоматически включает в файл .pas директиву <$R *.DFM>, обеспечивающую компоновку файлов ресурсов форм. Эту директиву нельзя удалять из текста модуля, так как в противном случае загрузочный модуль не будет создан и генерируется исключение EResNotFound.

7) Директивы компилятора, включающие и выключающие проверку диапазона целочисленных значений и индексов

По умолчанию <$R->или

Область действия локальная

Директивы компилятора $R включают или выключают проверку диапазона целочисленных значений и индексов. Если включена директива <$R+>, то все индексы массивов и строк и все присваивания скалярным переменным и переменным с ограниченным диапазоном значений проверяются на соответствие значения допустимому диапазону. Если требования
диапазона нарушены или присваиваемое значение слишком велико, генерируется исключение ERangeError. Если оно не может быть перехвачено, выполнение программы завершается.

Проверка диапазона длинных строк типа Long strings не производится.
Директива <$R+>замедляет работу приложения и увеличивает его размер. Поэтому она обычно используется только во время отладки.

8) Директива компилятора, связывающая с выполняемым модулем файлы ресурсов

Область действия локальная

Директива компилятора <$R>указывает файлы ресурсов (.DFM, .RES), которые должны быть включены в выполняемый модуль или в библиотеку. Указанный файл должен быть файлом ресурсов Windows. По умолчанию расширение файлов ресурсов — .RES.
В процессе компоновки компилированной программы или библиотеки файлы, указанные в директивах <$R>, копируются в выполняемый модуль. Компоновщик Delphi ищет эти файлы сначала в том каталоге, в котором расположен модуль, содержащий директиву <$R>, а затем в каталогах, указанных при выполнении команды главного меню Project | Options на странице Directories/Conditionals диалогового окна в опции Search path или в опции /R командной строки DCC32.

При генерации кода модуля, содержащего форму, Delphi автоматически включает в файл .pas директиву <$R *.DFM>, обеспечивающую компоновку файлов ресурсов форм. Эту директиву нельзя удалять из текста модуля, так как в противном случае загрузочный модуль не будет создан и генерируется исключение EResNotFound.

Профессиональная разработка приложений с помощью Delphi 5

Динамически загружаемые библиотеки (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:

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

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

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

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

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

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

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

DriveSoftware
Дата 22.1.2004, 14:05 (ссылка) | (нет голосов) Загрузка .
Директива Порядок следования параметров Очистка стека Использование регистров
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 с целочисленным идентификатором после него (идентификатор должен быть больше нуля), либо служебное слово name с текстовым идентификатором, либо оба вместе — как в данном случае. Внешний модуль может обращаться к конкретному методу как по индексу, так и по имени. Как это делается — будет рассказано в следующем разделе. На данном этапе изложения материала следует отметить, что название метода AddOne нигде и никогда не будет видно во внешних модулях — будет использоваться либо целочисленная константа 1, либо имя CalculateSum. Сразу же следует отметить, что имя чувствительно к регистру букв — метод не будет найден, если использовать, например, такие имена, как calculatesum или CALCULATESUM. Индексы, если они объявляются в секции exports, обязаны начинаться с 1 и принимать все целочисленные значения подряд (2, 3, 4…). Нельзя опускать какое-либо число в этой последовательности натуральных чисел — могут быть ошибки при импорте метода по индексу!

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

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

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

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

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

При нажатии кнопки будет вызываться метод из 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 выглядит следующим образом:

Первоначально определяется новый процедурный тип, например 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 выглядит следующим образом:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Вызов методов приложения в DLL

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

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

В приложении создается метод (не метод класса!), адрес которого передается в DLL. Для того чтобы данный метод можно было вызывать из DLL, созданных на других языках программирования, желательно использовать соглашение stdcall вызова при реализации указанных методов (так называемые callback-методы). Использование вызова метода в DLL можно проиллюстрировать на таком примере:

При необходимости вызвать метод объекта следует учитывать, что данный метод объекта характеризуется двумя адресами — адресом метода и адресом данных. Следовательно, необходимо передавать два указателя. Но вместо передачи двух указателей можно воспользоваться структурой TMethod, определенной в модуле SysUtils.pas:

Код в приложении для приведенного выше примера выглядит так:

Код в DLL, осуществляющий вызов метода объекта, выглядит следующим образом:

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

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

Обратите внимание: теперь в приложении (в проекте, где будет генерироваться *.exe-файл[U2] [NE3] ) определена секция exports! Соответствующий код в DLL для тестирования данного метода выглядит следующим образом:

При запуске этого проекта приложение автоматически загружает DLL и находит адрес метода в DLL CalculateNextExport (который экспортируется по имени SumExport). Загруженная DLL в качестве параметров получает заголовок модуля приложения и имя метода, под которым [U4] он экспортируется в приложении. Далее DLL использует метод GetProcAddress для нахождения адреса метода, экспортируемого приложением. Для того чтобы был возвращен адрес метода, в приложении объявляется секция exports, где описано внешнее имя метода. Этот пример показывает формально одинаковые структуру и способ формирования *.exe- и *.dll-файлов.

Модальные формы в DLL

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

Данный код вызова диалога следует применять только при статической загрузке DLL. При выполнении диалога в DLL нужно учитывать, что формы в DLL не могут создаваться вместе с запуском DLL, как это возможно в приложении при установке опции Auto-Create Form в разделе Project/Options/Forms. Поэтому форму необходимо создавать динамически, вызывая ее конструктор Create из кода. Соответственно перед выходом из процедуры, вызывающей форму, необходимо вызвать ее деструктор (в нашем примере — FDialog.Release). Необходимо учитывать, что в DLL создается объект типа TApplication. Поскольку и само приложение имеет данный объект, то (если не принимать никаких мер) на экране в панели приложений появятся две пиктограммы: одна — для приложения, другая — для DLL, выполняющей диалог (рис. 2).

При этом после нажатия на пиктограмму приложения оно активируется и всплывает главная форма, но доступ к элементам управления главной формы получить нельзя. Ясно, что такое поведение является некорректным. Поэтому в качестве параметра метода в DLL, выполняющего диалог, необходимо использовать ссылку на объект TApplication (точнее, его свойство Handle) приложения. Посредством присвоения Application.Handle:=AppHandle; в DLL уничтожается объект TApplication и приложение начинает поддержку рассылки сообщений элементам управления, созданным в DLL. В этом случае на панели задач присутствует одна пиктограмма приложения — это корректно. Приведем типичный пример кода основного приложения, вызывающего диалог из DLL:

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

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

В этом случае библиотека выгружается немедленно после выполнения диалога, а метод Release, который рекомендуется использовать для вызова деструктора формы, посылает сообщения CM_RELEASE самой форме посредством вызова метода PostMessage. Этот метод ставит сообщение в конец очереди, приложение продолжает выполнять код — и выгружает DLL! Только после выполнения кода начинается обработка очереди сообщений, в конце концов достается сообщение CM_RELEASE, делается попытка выполнить деструктор формы — но методы-то уже выгружены! Если операционная система обладает значительным резервом ресурсов, то велика вероятность, что место в ОЗУ, где хранился код, сохранит свое содержимое и форма диалога будет разрушена успешно. Но при небольшом количестве ресурсов на освободившееся место в ОЗУ немедленно помещаются какие-либо данные из виртуальной дисковой памяти, а попытка выполнить деструктор кончается исключением. Поэтому при динамической загрузке обязательно нужно использовать метод Free вместо Release. Кроме того, перед выгрузкой DLL рекомендуется вызвать метод Application.ProcessMessages.

Вторая проблема состоит в следующем. Если использовать один и тот же объект TApplication в приложении и DLL посредством выполнения приведенного выше оператора — Application.Handle:=AppHandle; — перед вызовом метода ShowModal в DLL, то после выгрузки DLL главная форма приложений минимизируется и ее необходимо восстанавливать вновь. Один из способов решения проблемы — вызвать метод ShowWindow(Handle,SW_RESTORE) сразу же после выполнения команды FreeLibrary в приложении. Однако при этом главная форма приложения будет мерцать. Другой способ — оставить разные объекты TApplication в приложении и DLL,— для этого не надо выполнять оператор Application.Handle:=AppHandle; в DLL. Но тогда на панели задач появляются две пиктограммы. Корректное решение проблемы — присвоить нулевое значение свойству Application.Handle в DLL перед выходом из метода. Ниже приведен рекомендуемый код в DLL, которая выполняет диалог при динамической загрузке:

Приведем код приложения, динамически вызывающего данную DLL:

Немодальные формы в DLL

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

Как и при показе модальных форм, необходимо присвоить дескриптор окна главного приложения приложению, создаваемому в DLL, — иначе на панели задач будут показаны две иконки. Затем просто создается форма и вызывается ее метод Show. Такой способ показа немодальных форм приводит к тому, что из главного приложения можно неоднократно вызвать данный метод и тем самым создать несколько экземпляров форм. Зачастую это оправданно, поскольку одинаковые типы форм могут содержать, например, разные документы. Но при этом способе показа рекомендуется сделать обработчик события OnClose для TForm1 и присвоить параметру CloseAction значение caFree — в противном случае при закрытии форма будет спрятана [NE5][U6] на экране без освобождения системных ресурсов.

Для показа единственного экземпляра немодальной формы следует немного изменить код:

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

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

Иногда возникает необходимость показа немодальных форм из динамически загружаемых DLL, например при редком использовании в приложении немодальных форм для экономии ресурсов. Если реализовать код так же, как и при показе модальных диалогов, то форма будет создана и, может быть, даже показана на экране. Но после этого произойдет выгрузка DLL, а затем немедленно последуют исключения, поскольку в памяти компьютера будет отсутствовать код для работы с элементами управления формы. Традиционное решение этой проблемы выглядит следующим образом: загружается динамическая библиотека, в качестве одного из параметров передается адрес метода главного приложения, который будет вызван при закрытии немодальной формы — обычно в обработчике события OnDestroy. Этот метод должен информировать главное приложение о необходимости выгрузки DLL из памяти компьютера, но DLL должна выгружаться после завершения его работы (и, следовательно, после завершения работы деструктора формы) — иначе возможно исключение из-за отсутствия кода в памяти компьютера. Выгрузка DLL после завершения работы приложения [U7] достигается с использованием асинхронной развязки — посылки сообщения методом PostMessage какому-либо окну приложения, обычно главной форме. Приведем код реализации данной технологии в DLL:

Приложение, использующее эту DLL, имеет следующий код (WM_DLLUNLOAD определена как константа в секции interface модуля):

Очевидно, что код получается довольно громоздким: в главном приложении необходимо реализовывать три метода вместо одного. Альтернативный вариант можно предложить исходя из того, что в DLL имеется объект TApplication, который может поддерживать цикл выборки сообщений. Но в DLL нельзя создать форму, используя метод TApplication. CreateForm, так как соответствующая закладка диалога Project/Options/Forms отсутствует в проектах Delphi 4 и 5 и неактивна в Delphi 3. Однако можно вызвать все методы объекта Tapplication, вручную дописав соответствующий код в DLL:

Следует обратить внимание, что дескриптор главного приложения не присваивается в данном проекте дескриптору TApplication в DLL. Это реально приводит к появлению двух пиктограмм на панели приложений. Правда, в некоторых случаях это полезно — так легче добраться до перекрытых окон. Интересно отметить, что в Delphi 3 после написания данного кода становятся доступными элементы управления диалога Project/Options/Forms, где можно определить автоматически создаваемые формы и главную форму приложения. Код главного приложения, использующий данную DLL, таков:

В отличие от предыдущего примера, динамическая загрузка DLL и ее выгрузка осуществляются в одном методе, да и объем написанного кода существенно меньше.

Экспорт дочерних форм из DLL

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

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

На форму TForm1 помещают элементы управления, которые необходимо показать в главном приложении. Обычно свойство BorderStyle дочерних форм устанавливают равным bsNone (рис. 3).

Далее, дочерним формам в качестве параметров метода следует передавать прямоугольную область родительского окна, в которой она размещается. Это означает, что дочерняя форма должна иметь хорошо отлаженные обработчики событий, которые связаны с изменением ее размеров. Как и во всех предыдущих примерах, показ дочерних форм следует сделать независимым от языка; следовательно, необходимо использовать методы Windows API для изменения родителей. Такой метод определен в модуле user32.dll — SetParent. Другой метод Windows API — SetWindowPos — используется для изменения границ формы. В качестве возвращаемого параметра возвращается указатель на объект. Но приложение не будет его использовать, поскольку оно должно его запоминать и использовать при вызове метода DeleteCustomWindow. Поэтому сохраняется возможность использовать данный проект в приложениях, написанных не на Delphi, а на других языках.

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

Код основного приложения для тестирования данной DLL выглядит следующим образом:

В этом проекте на форму TForm2 никакие элементы управления не помещаются. Главная форма приложения содержит одну кнопку, при нажатии на которую выполняется немодальный показ TForm2:

with TForm2.Create(Self) do Show;

При создании второй формы происходит загрузка DLL, а форма, созданная в DLL, становится дочерней для TForm2. Можно создать несколько экземпляров TForm2. При разрушении конкретного экземпляра разрушается и дочернее окно на нем, для чего используется сохраненный ранее параметр FchildID (рис. 4).

Казалось бы, аналогичную методику можно использовать для экспорта из DLL других элементов управления, среди предков которых не содержится класс TForm. Однако при попытке использовать метод SetParent непосредственно для элементов управления происходит генерация исключения об отсутствии у элемента управления родительского окна, и этот элемент [U8] не показывается на форме.

Заключение

Итак, с помощью динамически загружаемых библиотек можно оптимизировать ресурсы, необходимые для выполнения приложений; использовать в проектах модули, написанные на различных языках программирования; создавать проекты, которые могут иметь необязательные функции и пункты меню. Вызов методов из DLL не представляет трудностей, за исключением того, что следует обращать особое внимание на исключительные ситуации: не допускать попадания экземпляра — потомка Exception в главный модуль, обязательно вызывать команду FreeLibrary при наличии исключений. Анализу исключительных ситуаций и отладке приложений будет посвящена следующая статья данного цикла.

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

Все о программировании.

Главное меню

Написание DLL в Delphi

Самое простое — писать функции и процедуры DLL с передачей целых чисел или с плавающей точкой. Тестовый проект Delphi демонстрирует требуемые объявления в Delphi и Visual Basic:

Модуль, который содержит код DLL показан ниже. Обратите внимание на объявление в разделе interface и использование директивы export после объявления. Директива export делает процедуру или фукнцию экспортной в пределах DLL, вынуждая подпрограмму использовать модель дальнего вызова и генерируя специальный вход в процедуру и код завершения.

Это код тестовой формы, которая содержит две кнопки:

чтобы проверить приложение, скомпилируйте и запустите проект обычным способом. Для создания проекта DLL, используйте rgdll.dpr, показанный выше и скомпилируйте проект. Вы не можете запустить проект DLL.

Теперь давайте создадим проект в VB. Нужно создать форму с двумя кнопками и файлом модуля. В модуле поместите следующие объявление:

Нужно отметить следующее:

  1. Каталог, в котором расположена DLL должен быть в стандартной папке, обычно Windows/System
  2. Типы параметров, которые передают DLL, должны соответствовать
  3. Используйте ByVal для передачи значений

В коде формы поместите следующее:

Если все работает, значит Вы создали свою первую DLL!

Илон Маск рекомендует:  Посчитать модуль числа в Excel
Понравилась статья? Поделиться с друзьями:
Кодинг, CSS и SQL