пятница, 12 апреля 2013 г.

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

Перевод раздела Writing Dynamically Loaded Libraries из справочной системы Delphi

Замечание: Библиотеки в части экспорта ограничены значительно больше, чем пакеты. Библиотеки не могут экспортировать константы типы и обычные переменные. То есть класс, объявленный в библиотеке, не может быть виден в программе, которая ее использует.

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



Использование раздела Export в библиотеках

Основной файл динамически загружаемой библиотеки начинается с зарезервированного слова library, в остальном он идентичен основному файлу программы (который начинается с зарезервированного слова program).

Для экспорта в другие библиотеки или программы доступны только подпрограммы, которые явным образом экспортируются библиотекой. Следующий пример показывает библиотеку, экспортирующую две функции: Min и Max.

library MinMax;
function Min(X, Y: Integer): Integer; stdcall;
begin
  if X < Y then Min := X else Min := Y;
end;
function Max(X, Y: Integer): Integer; stdcall;
begin
  if X > Y then Max := X else Max := Y;
end;
exports
  Min,
  Max;
  begin
 end.

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

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

library Editors;
uses EdInit, EdInOut, EdFormat, EdPrint;
exports
  InitEditors,
  DoneEditors name Done,
  InsertText name Insert,
  DeleteSelection name Delete,
  FormatSelection,
  PrintSelection name Print,
    .
    .
    .
  SetErrorHandler;
 begin
   InitLibrary;
 end.

Разделы exports в модуле можно разместить в секции interface или implementation. Библиотека, которая включает в себя такой модуль, автоматически экспортирует все подпрограммы из его разделов exports и не требует наличия собственного раздела exports.

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

exports entry1, ..., entryn;

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

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

На платформе Win32 спецификатор index состоит из директивы index, за которой следует числовая константа в диапазоне от 1 до 2,147,483,647. (Для повышения эффективности работы программы следует использовать небольшие значения индекса). Если элемент в списке не имеет спецификатора индекса, подпрограмме автоматически присваивается ее номер в таблице экспорта.

Замечание: Использование спецификаторов index не рекомендовано и поддерживается только для обратной совместимости. Их применение может привести к проблемам в работе инструментария разработчика.

Спецификатор имени состоит из директивы name, за которой следует строковая константа. Если элемент списка не содержит такого спецификатора, подпрограмма экспортируется под своим оригинальным именем (как она была объявлена) с учетом его написания и регистра символов в названии. Спецификатор name следует использовать в том случае, если вы хотите экспортировать подпрограмму под именем, отличным от ее объявления. Например:

exports
DoSomethingABC name 'DoSomething';

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

exports
Divide(X, Y: Integer) name 'Divide_Ints',
Divide(X, Y: Real) name 'Divide_Reals';

На платформе Win32 при экспорте перегружаемых подпрограмм не следует указывать спецификаторы индексов.

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

Код инициализации в библиотеке

Инструкции в блоке библиотеки представляют собой код инициализации. Эти инструкции выполняются однократно в момент загрузки библиотеки. Обычно они выполняют задачи регистрации классов окон и инициализации переменных. В коде инициализации библиотеки можно также, используя переменную DllProc, выполнить установку процедуру точки входа. Переменная DllProc схожа с процедурой выхода, которая описана в разделе Exit procedures. Процедура точки входа выполняется в момент загрузки или выгрузки библиотеки.

Код инициализации библиотеки может передать информацию об ошибке, устанавливая значение, отличное от нуля для переменной ExitCode. ExitCode объявлена в модуле System и по умолчанию имеет значение 0, указывая на успешную инициализацию. Если код инициализации библиотеки устанавливает для ExitCode ненулевое значение, библиотека выгружается, а вызывающее приложение получает уведомление об ошибке. Вызывающее приложение получает такое же уведомление в случае, когда при выполнении кода инициализации, возникает необработанная исключительная ситуация.

Далее приведен пример библиотеки с кодом инициализации и процедурой точки входа:

library Test;
var
  SaveDllProc: Pointer;
procedure LibExit(Reason: Integer);
begin
  if Reason = DLL_PROCESS_DETACH then
  begin
    .
    .   // library exit code
    .
  end;
  SaveDllProc(Reason); // call saved entry point procedure
end;
begin
    .
    .   // library initialization code
    .
  SaveDllProc := DllProc; // save exit procedure chain
  DllProc := @LibExit;   // install LibExit exit procedure
end.

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

Глобальные переменные в библиотеке

Глобальные переменные, объявленные в библиотеке, не могут быть импортированы приложением Delphi.

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

Библиотеки и системные переменные

Кроме того, для разработчиков библиотек представляют особый интерес еще несколько переменных, объявленных в модуле System. Для того, чтобы определить, какой программный код выполняется в текущий момент, можно воспользоваться переменной IsLibrary. IsLibrary всегда имеет значение False в приложении и True – в библиотеке. В течение всего времени жизни библиотеки переменная HInstance содержит дескриптор экземпляра (instance handle). При работе с библиотекой значение CmdLine всегда nil.

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

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

procedure DLLHandler(Reason: Integer);

Передайте адрес процедуры в переменную DLLProc. При вызове процедуры она будет передавать одно из следующих значений.

DLL_PROCESS_DETACHУказывает, что библиотека выводится из адресного пространства вызывающего процесса в результате корректного завершения работы или вызова FreeLibrary
DLL_PROCESS_ATTACHУказывает, что библиотека вводится в адресное пространство вызывающего процесса в результате вызова LoadLibrary
DLL_THREAD_ATTACHУказывает, что текущий процесс создает новый поток
DLL_THREAD_DETACHУказывает, что выполняется корректное завершение работы потока

В теле процедуры можно определить поведение в зависимости от значения этого параметра.

Исключения и ошибки выполнения (run time errors) в библиотеках

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

На платформе Win32, при вызове приложения или библиотеки, разработанной на другом языке, исключение может быть обработано как исключение операционной системы с кодом $0EEDFADE. Первый элемент в массиве ExceptionInformation, включенном в запись операционной системы об исключении, содержит адрес исключения, а следующий элемент – ссылку на объект исключения Delphi.

Обычно не следует выводить исключения из библиотеки. Исключения Delphi сопоставляются с моделью исключений операционной системы.

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

Менеджер разделяемой памяти (Shared-Memory Manager)

На платформе Win32, если DLL экспортирует подпрограммы, которые передают как параметры или результаты функций длинные строки или динамические массивы (в чистом виде или в составе записей или объектов), DLL и его клиентские приложения (или DLL) должны подключать модуль ShareMem. Это также верно в том случае, когда приложение или DLL выделяет память при помощи New или GetMem, которая освобождается вызовом Dispose или FreeMem в другом модуле. В программе или библиотеке модуль ShareMem всегда должен указываться на первом месте в разделе uses.

ShareMem – это интерфейсный модуль для менеджера памяти BORLANDMM.DLL, который позволяет модулям разделять динамическую память. BORLANDMM.DLL должен быть развернут вместе с приложением и библиотеками, которые подключают ShareMem. Если приложение или библиотека подключает ShareMem, его менеджер памяти заменяется менеджером памяти из BORLANDMM.DLL.

Комментариев нет:

Отправить комментарий