Шаг 302.
Создание внутрипроцессных серверов автоматизации. Создание и использование DLL. Создание простейшей библиотеки

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

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

library FirstLib;

uses
  SysUtils,
  Classes;

{$R *.res}

begin
end.

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

function AddOne(N: Integer): Integer; stdcall; export;
begin
  Result := N + 1;
end;

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

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


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

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

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

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

    Альтернатива заключается в том, что в начале в стек может быть помещена константа 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 1 name 'CalculateSum';
Текст этой библиотеки можно взять здесь (50,2 Кб).

    Для экспонирования функции в секции 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.




Предыдущий шаг Содержание Следующий шаг