Глава 2. 32-разрядные DLL в Delphi — когда, зачем и как

Джим Мишель

VCL-компоненты открывают новые возможности для многократного использования кода, но даже древние механизмы — такие как Windows DLL — при разумном применении способны творить чудеса.

Весна началась интересно. В феврале было холодно — здесь, в Остине, даже пошел снег. Дороги заледенели, машины разбивались буквально на каждом углу. Неплохое развлечение, если только в нем не участвует твоя машина. Вскоре после снегопада у нашего старенького «Бронко» забарахлил водяной насос и прохудился уплотнитель, и мы решили, что настало время подумать о новой машине. Вы не приценивались к так называемым «недорогим машинам»? Просто ужас! 

Следующим вопросом на повестке дня оказался фильтр плавательного бассейна. В апреле у нас уже купаются, поэтому я открыл эту штуковину (какой странный оттенок зеленого…) и включил насос. Ни капли. Пришел спец по бассейнам и все исправил, но в итоге я стал заметно беднее. Потом засорилась система очистки воды, потому что идиот подрядчик сэкономил 20 долларов и поставил между домом и резервуаром ненадежную трубу. Водопроводчик содрал еще больше, чем спец по бассейнам. Короче, обитателям chez Mischel эти два месяца обошлись довольно дорого. 

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

В жизни гибкость обычно обеспечивается денежными затратами. При программировании для Windows гибкость достигается с помощью DLL. 

Что такое DLL и зачем они нужны? 

DLL (Dynamic Link Library, библиотека динамической компоновки) — разновидность выполняемых файлов Windows, в которых содержится код или данные, используемые другими программами. По своей концепции DLL напоминают модули Delphi, они тоже представляют собой «упакованные» фрагменты кода, с помощью которых ваша программа может выполнять различные действия. Концепция похожа — но с ее реализацией дело обстоит совершенно иначе. 

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

Если у вас имеется большой модуль, который используется многими программами, ваши программы будут содержать большое количество повторяющегося кода. Хотя дисковое пространство сейчас обходится примерно в 30 центов за мегабайт и проблема стала не столь актуальной, как раньше (здесь мы не будем обращать внимания на минимальный размер сектора), что произойдет, если вам потребуется запустить четыре или пять таких программ одновременно? В итоге код модуля будет дублироваться в памяти. Память тоже не так уж дорога, но и дешевой ее не назовешь — во всяком случае настолько дешевой, чтобы расходовать ее понапрасну. 

Вторая причина, по которой статическая компоновка может оказаться нежелательной, — гибкость. Предположим, вы только что написали новейший текстовый редактор, настоящее программное чудо, и теперь хотите научить его импортировать документы из других файловых форматов (это необходимо сделать, чтобы выдержать конкуренцию на рынке текстовых редакторов). Конечно, можно написать специальный модуль для каждого распространенного файлового формата и выбросить продукт на рынок. Но через полгода выходит новая версия какого-нибудь Word Grinder Max (надеюсь, продукта с таким названием в действительности не существует) с новым форматом, и ваша программа устаревает! Единственный способ выйти из положения и научить программу работать с новым форматом — выпустить обновление, на котором вы не заработаете ничего, кроме хлопот. Кроме того, снова возникает проблема размера. При статической компоновке кода для работы с сотнями разных форматов ваша программа будет перегружена огромным количеством балласта — кода, который использу ется очень редко или нужен очень узкому кругу клиентов. 

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

Вот что является подлинной гибкостью. 

Как это сделать? 

Тема DLL включает в себя два вопроса: создание и использование. Вы ежедневно используете DLL при работе под Windows, возможно, даже не подозревая об этом. Почти вся система Windows реализована в виде различных DLL. Например, при вызове функции MessageBox происходит обращение к DLL с именем USER.EXE (или USER32.DLL — Windows 95 порой производит довольно странные манипуляции с автоматическим 32/16-разрядным преобразованием (thunking), поэтому я не всегда понимаю, что именно происходит в системе). Независимо от того, знаете вы это или нет, DLL используются в вашей повседневной работе. 

Существуют два способа вызова функций, находящихся в DLL. Вы можете построить интерфейсный модуль, в котором указано имя DLL и вызываемых функций, и связать его со своей программой на Delphi. Это называется статической компоновкой DLL (на мой взгляд, термин неудачен, но его придумал не я) . Также иногда встречается термин «динамическая компоновка на стадии компиляции» 1. Именно так вызываются функции Windows API. Файл WINDOWS.DCU, подключаемый ко всем программам, у которых в операторе uses указан модуль Windows, представляет собой именно такой интерфейсный модуль с определениями функций. 

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

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

Создание DLL 

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

Листинг 2.1. Простейшая DLL

{
  BEEPER.DPR — пример простейшей DLL

  Автор: Джим Мишель
  Дата последней редакции:  12/05/97
}
library beeper;

uses Windows;

procedure BeepMe; stdcall;
begin
  MessageBeep (0);
end;

Exports
  BeepMe index 1 name "BeepMe";

begin
end.

DLL начинаются с ключевого слова library вместо привычного program или unit. В них также имеется оператор uses. Также обратите внимание — DLL, как и программы, не имеют отдельных секций interface и implementation. Процедуры и функции DLL пишутся точно так же, как и процедуры/функ ции программ, но их необходимо явным образом экспортировать (используя ключевое слово Exports), чтобы они стали доступны для других программ. 

Присутствие ключевого слова stdcall необязательно, но его все же стоит включать. Экспортированные функции DLL, имеющие модификатор stdcall, совместимы с другими языками программирования (например, C++), которые могут обращаться к DLL. Наличие stdcall никак не отражается на работе экспортированных функций. Я рекомендую включать stdcall, если экспортированные функции могут вызываться из программ, написанных на C/C++. 

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

Exports
BeepMe index 1 name "BeepMe",
PageMe index 2 name "PageMe"; 

Чтобы создать DLL, выполните команду File д New и выберите DLL в диалоговом окне New Items. Введите содержимое листинга 2.1, сохраните его под именем BEEPER.DPR и затем откомпилируйте. Напрямую запустить DLL не удастся — для этого нужна другая (вызывающая) программа. 

Вызов функций DLL 

После завершения компиляции сохраните проект и выполните команду File д New Application. Сейчас мы напишем простейшую тестовую программу для вызова DLL. 

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

procedure TForm1.Button1Click(Sender: TObject);
begin
BeepMe;
end; 

Включите BeepDLL в список, следующий за ключевым словом uses в модуле формы. Не пытайтесь компилировать программу, сначала нужно создать файл BEEPDLL.PAS

Создайте новый модуль с именем BEEPDLL.PAS и введите содержимое листинга 2.2. 

Листинг 2.2. Интерфейсный модуль для BEEPER.DLL

{ BEEPDLL.PAS — интерфейсный 
модуль для BEEPER.DLL }
unit BeepDLL;

interface
procedure BeepMe; external 
"beeper.dll";
procedure BeepMeTwo; external 
"beeper.dll" 
name "BeepMe";
procedure BeepMeThree; 
external "beeper.dll" index 1;

implementation
end.

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

Наверное, вы заметили, что для вызова процедуры BeepMe из BEEPER.DLL я указал целых три разных варианта. Если бы обработчик нажатия кнопки вызывал BeepMeThree вместо BeepMe, результат остался бы прежним. Мы работаем с искусственным примером, но в некоторых ситуациях возможность подключения функций DLL по имени (name) или номеру (index) оказывается полезной. Пусть, например, вам требуется вызвать из DLL функцию с именем XY$FORMAT (вполне реальный пример). Поскольку в Паскале XY$FORMAT не является допустимым идентификатором, вам не удастся воспользоваться этой функцией без ее переименования (см. вариант BeepMeTwo). Столь же полезно и ключевое слово index: некоторые функции DLL экспортируются только по номеру, без имени! 

Мы рассмотрели пример статического импорта DLL. Интерфейсный модуль BEEPDLL.PAS всего лишь сообщает компилятору о том, что процедуру BeepMe необходимо взять из файла BEEPER.DLL посредством динамической компоновки. Код, содержащийся в BEEPER.DLL, не включается в вашу программу. Если не верите, удалите BEEPER.DLL и попробуйте снова запустить программу. Если программа была запущена из IDE, Delphi выдаст сообщение об ошибке. Если же запустить программу автономно, Windows сообщит о том, что ей не удалось найти библиотеку BEEPER.DLL

Это сообщение об ошибке подводит нас к другому способу вызова функций DLLАF0;— динамическому импорту. 

Присоединение DLL на стадии выполнения 

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

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

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

Глава 2•32-разрядные DLL в Delphi — когда, зачем и как

В листинге 2.3 содержится новая версия интерфейсного модуля BEEPER.DLL. Директивы условной компиляции позволяют выбрать тип импорта — статический или динамический. 

Листинг 2.3. Динамический импорт DLL на стадии выполнения

{
  BEEPDLL.PAS — интерфейсный модуль 
  для BEEPER.DLL

  Автор: Джим Мишель
  Дата последней редакции:  12/05/97
}
unit BeepDLL;


{$DEFINE DYNAMIC}  { закомментируйте эту строку, 
чтобы реализовать статический импорт }

interface

{$IFDEF DYNAMIC}
  { Объявления процедур 
  для динамического импорта }
  procedure BeepMe;
  procedure BeepMeTwo;
  procedure BeepMeThree;
{$ELSE}
  { Объявления процедур для статического 
  импорта }
  procedure BeepMe; external 
  "beeper.dll";
  procedure BeepMeTwo; 
  external "beeper.dll" 
  name "BeepMe";
  procedure BeepMeThree; 
  external "beeper.dll" index 1;
{$ENDIF}

implementation

{$IFDEF DYNAMIC}
uses Windows;

type
  BeepMeProc = procedure;

var
  LibInstance : HMODULE;       
  { Логический номер модуля DLL }
  BeepMePtr : BeepMeProc;

procedure BeepMe;
begin
  if (LibInstance = 0) then begin
    { если DLL еще не загружена, 
    попытаемся загрузить }
    LibInstance := LoadLibrary("beeper.dll");
    { Если LoadLibrary возвращает 0, произошла ошибка }
    if (LibInstance = 0) then begin
      MessageBox (0, "Can"'t load BEEPER.DLL', "Error",
        MB_ICONEXCLAMATION or MB_OK);
      Exit;
    end;

    { DLL загружена, теперь попытаемся 
    найти функцию }
    BeepMePtr := BeepMeProc (GetProcAddress (LibInstance, "BeepMe"));
    { Если GetProcAddress возвращает Nil, у нас возникли проблемы}
    if (Not Assigned (BeepMePtr)) then begin
      { Предварительно выгрузим DLL, чтобы пользователь
        заменил ее, если это возможно }
      FreeLibrary (LibInstance);
      LibInstance := 0;
      MessageBox (0, "Can"'t find BeepMe function in DLL.', "Error",
        MB_ICONEXCLAMATION or MB_OK);
      Exit;
    end;
  end;

  BeepMePtr;
end;

procedure BeepMeTwo;
begin
  BeepMe;
end;

procedure BeepMeThree;
begin
  BeepMe;
end;

initialization
  LibInstance := 0;
  BeepMePtr := Nil;

finalization
  { Если DLL была загружена, ее обязательно 
  нужно выгрузить }
  if (LibInstance <> 0) then begin
    FreeLibrary (LibInstance);
    LibInstance := 0;
  end;

end.
{$ELSE}
end.

{$ENDIF}

Я же предупреждал, что этот вариант сложнее! 

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

Прежде всего, имена процедур не связываются с функциями DLL непосредственно в интерфейсной (interface) секции модуля, а соответствуют обычным процедурам, определенным в секции реализации (implementation). Именно ключевое слово external вызывает автоматическую загрузку DLL при запуске программы; если удалить его, Windows не станет загружать DLL. 

Затем мы определяем процедурный тип и две переменные: 

type
BeepMeProc = procedure;

var
LibInstance : HMODULE; { Логический номер экземпляра DLL }
BeepMePtr : BeepMeProc; 

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

LibInstance — логический номер (handle) экземпляра BEEPER.DLL, который возвращается функцией LoadLibrary, если загрузка DLL прошла успешно. 

Процедуры BeepMeTwo и BeepMeThree являются псевдонимами для BeepMe, поэтому в версии с динамическим импортом они просто вызывают процедуру BeepMe модуля. 

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

Если функция LoadLibrary успешно загрузила DLL, мы вызываем функцию GetProcAddress, которая пытается найти в загруженной DLL функцию с именем BeepMe. Адрес найденной функции присваивается переменной BeepMePtr. Если GetProcAddress не может найти заданную функцию, она возвращает Nil, в результате чего BeepMe выдает сообщение об ошибке и выгружает DLL из памяти. 

Если все прошло нормально, то есть DLL была успешно загружена, а процедура BeepMe — найдена, она вызывается через указатель BeepMePtr.

Последнее замечание — ваша программа должна явно выгрузить (используя процедуру FreeLibrary) все DLL, загруженные с помощью LoadLibrary. Для этого используются секции initialization и finalization. При запуске модуля секция initialization присваивает переменным LibInstance и BeepMePtr стандартные значения, означающие, что DLL не загружена. При выходе из программы секция finalization выгружает DLL, если она была загружена ранее. 

Где Windows ищет DLL 

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

Итак, Windows ищет DLL в следующих местах (и в следующем порядке): 

  1. Каталог, из которого было загружено приложение. 
  2. Текущий каталог. 
  3. Системный каталог Windows. 
  4. Только для Windows NT: системный каталог 16-разрядной Windows. 
  5. Каталог Windows.
Каталоги, перечисленные в переменной окружения PATH

В случае динамического импорта при вызове LoadLibrary можно указать для DLL полный путь, тогда Windows просмотрит только заданный каталог. Если вы хотите, чтобы Windows автоматически загружала DLL при запуске (статический импорт), такой возможности уже не будет. 

DLL: недостатки и предостережения 

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

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

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

procedure BeepMe; external "beeper.dll"; 

Данное объявление просто сообщает компилятору о том, что существует некая процедура BeepMe и она находится в указанной DLL. Замечательно. Компилятор верит вам на слово. Он никак не может найти файл BEEPER.DLL, дизассемблировать его и убедиться, что в нем действительно есть процедура с именем BeepMe и что она вызывается без параметров. Если процедура BeepMe в DLL должна получать один или несколько параметров (или в случае процедуры с параметрами — параметры другого типа), при вызове BeepMe разверзнется сущий ад: процедура получит неверное количество параметров или они будут иметь неверный тип. Гарантирую, что это когда-нибудь случится и с вами. По своему опыту знаю, что найти подобную ошибку очень сложно. Стыдно признаваться, но я и сам столкнулся с этой проблемой вскоре после того, как написал предыдущую фразу, во время работы над программой для следующего раздела. 

Если вас интересует более подробное (и устрашающее) описание проблем, связанных с DLL, почитайте книгу Лу Гринзо (Lou Grinzo) «Zen of Windows 95 Programming» (Coriolis Group Books, 1995). Эта превосходная книга содержит массу полезной информации о программировании для Windows, а также ряд хороших советов по поводу программирования вообще. Для программирования необходима паранойя (в разумных дозах) и твердая вера в справедливость законов Мерфи. Даже если вы не верите в это сейчас, то после прочтения книги Лу непременно поверите. 

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

Создание форм в DLL 

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

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

В листингах 2.4 и 2.5 содержатся исходные тексты файлов PICKCLR.DPR и COLORFRM.PAS, которые реализуют форму для выбора цвета в виде DLL. 

Листинг 2.4. Файл PICKCLR.DPR

{
  PICKCLR.DPR — DLL с формой для 
  выбора цвета

  Автор: Джим Мишель
  Дата последней редакции:  12/05/97
}
library pickclr;

uses
  SysUtils,
  Classes,
  ColorFrm in "colorfrm.pas" 
  {ColorSelectionForm};

Exports
  ColorFrm.PickColors index 1 name 
  "PickColors";

begin
end.

Листинг 2.5. Модуль COLORFRM.PAS

{
  COLORFRM.PAS — Выбор цвета с помощью 
  формы, хранящейся в DLL

  Автор: Джим Мишель
  Дата последней редакции:  12/05/97
}
unit colorfrm;

interface

uses
  Windows, Messages, SysUtils, Classes, 
  Graphics, Controls, Forms,
  Dialogs, StdCtrls, ColorGrd;

type
  TColorSelectionForm = class(TForm)
    ColorGrid1: TColorGrid;
    BtnOk: TButton;
    BtnCancel: TButton;
  private
    { Private declarations }
  public
    { Public declarations }
    function Execute : boolean;
  end;

function PickColors (var Foreground, 
Background : TColor) : boolean;
    stdcall; export;

implementation

{$R *.DFM}

function TColorSelectionForm.Execute : boolean;
begin
  Result := (ShowModal = mrOk);
end;

function PickColors (var Foreground, 
Background : TColor) : boolean; stdcall;
var
  ColorForm : TColorSelectionForm;
begin
  ColorForm := 
   TColorSelectionForm.Create (Application);
  Result := ColorForm.Execute;
  if (Result = True) then begin
    Foreground := ColorForm.ColorGrid1.ForegroundColor;
    Background := ColorForm.ColorGrid1.BackgroundColor;
  end;
  ColorForm.Free;
end;

end.

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

Как видно из листинга 2.4, файл проекта для DLL выглядит очень просто. Главное — правильно написать секцию Exports. Чтобы добавить в DLL другие формы, достаточно включить имена их модулей в секцию uses и добавить определения функций-оболочек в секцию Exports.

Интерфейсный модуль любой DLL с формами должен напоминать BEEPDLL.PAS из листинга 2.3. Как и BEEPDLL, он должен предоставлять возможность выбора между статическим и динамическим импортом. Дляэкономии места я не стал приводить интерфейсный модуль для PICKCLRDLL.

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

Гибкое кодирование 

Многие продукты содержат специальные «точки входа» (hooks), через которые к ним можно подключить дополнительные модули, выпущенные независимыми фирмами. Например, в Windows Help определен интерфейс, с помощью которого разработчики могут включать в справочные файлы Windows нестандартные макросы и вспомогательные окна, добиваясь очень интересных эффектов. Интегрированная среда Borland C++ 5.0 также содержит интерфейс, с помощью которого в нее можно добавлять новые возможности. В комплект BC++ 5.0 входят модуль поддержки групповой разработки (контроля версий файлов) и дополнение для работы на Java, реализованные в виде DLL и подключенные через интерфейс расширения. 

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

Создание текстового редактора 

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

Создайте новый проект, поместите на форму компонент Memo и задайте его свойству Align значение alClient, чтобы он занял всю форму. Затем добавьте на форму компоненты MainMenu, OpenDialog и SaveDialog. В режиме конструирования меню добавьте три команды: Open, Save и Exit. Сохраните модуль формы в виде файла EDITFORM.PAS, а файл проекта — под именем TEXTEDIT.DPR. Готовая форма показана на рис. 2.1, а текст программы содержится в листинге 2.6. 

Рис. 2.1. Готовая форма текстового редактора 

Листинг 2.6. Форма текстового редактора, EDITFORM.PAS

{
  EDITFORM.PAS — Простейший текстовый 
  редактор, демонстрирующий
                  использование DLL 

  Автор: Джим Мишель
  Дата последней редакции:  12/05/97
}
unit editform;

interface

uses
  Windows, Messages, SysUtils, Classes, 
  Graphics, Controls, Forms,
  Dialogs, Menus, StdCtrls;

type
  TForm1 = class(TForm)
    Memo1: TMemo;
    OpenDialog1: TOpenDialog;
    SaveDialog1: TSaveDialog;
    MainMenu1: TMainMenu;
    File1: TMenuItem;
    Open1: TMenuItem;
    Save1: TMenuItem;
    N1: TMenuItem;
    Exit1: TMenuItem;
    procedure Exit1Click(Sender: TObject);
    procedure Open1Click(Sender: TObject);
    procedure Save1Click(Sender: TObject);
  private
    { Private declarations }
    FileName : String;
    procedure OpenFile(Filename: String);
    procedure SaveFile(Filename: String);
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.DFM}
uses IniFiles;
procedure TForm1.Exit1Click(Sender: TObject);
begin
  Close;
end;

procedure TForm1.Open1Click(Sender: TObject);
begin
  if OpenDialog1.Execute then
    OpenFile (OpenDialog1.FileName);
end;

procedure TForm1.Save1Click(Sender: TObject);
begin
  if SaveDialog1.Execute then
    SaveFile (SaveDialog1.FileName);
end;

procedure TForm1.OpenFile (Filename: String);
begin
  Memo1.Lines.LoadFromFile(Filename);
end;

procedure TForm1.SaveFile (Filename: String);
begin
  Memo1.Lines.SaveToFile (Filename);
end;

end.

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

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

Общая идея — поместить в INI-файл описание файлового формата, стандартное расширение и имя DLL, выполняющей преобразование. Пример такого INI-файла приведен в листинге 2.7. 

Листинг 2.7. Файл TEXTEDIT.INI

; TEXTEDIT.INI
; Пример интерфейса расширения для файловых 
преобразований
[Text]
Extension=.TXT
ConvertDLL=textconv.dll
[Word for Windows]
Extension=.DOC
ConvertDLL=wfwconv.dll

[WordCruncher]
Extension=.WCX
ConvertDLL=wcxconv.dll

Нам придется изменить процедуру OpenFile так, чтобы она просматривала расширение имени открываемого файла и затем вызывала функцию преобразования из соответствующей DLL. Функция читает файл, преобразовы вает текст и возвращает результат в виде списка строк. Для выполнения всех преобразований используется функция Convert, вызываемая из текстового редактора. В листинге 2.8 содержится новый вариант функции OpenFile (не забудьте добавить модуль IniFiles в строку uses модуля формы), а в листингах 2.9 и 2.10 — исходный текст DLL текстовых преобразований (TEXTCONV.DLL). 

Листинг 2.8. Новая функция OpenFile

procedure TForm1.OpenFile (Filename: String);
type
  ConvertFunc = function (Filename: String;
      Strings: TStrings): boolean; stdcall;
var
  ConvertIni : TIniFile;
  ConvertList : TStringList;
  FileExt : String;
  Extension : String;
  DLLName : String;
  x : Integer;
  Found : Boolean;
  LibInstance : HMODULE;
  Converter : ConvertFunc;
  IniFileName : String;

begin
  FileExt := UpperCase 
  (ExtractFileExt (Filename));
  IniFileName := ExtractFileDir 
  (ParamStr (0)) + "\TEXTEDIT.INI";
  ConvertIni := TIniFile.Create (IniFileName);
  ConvertList := TStringList.Create;

  { Считываем список возможных преобразований }
  ConvertIni.ReadSections (ConvertList);

  {
    Для каждого преобразования читаем значение 
    Extension
    и сравниваем его с расширением 
    выбранного файла.
  }
  x := 0;
  Found := False;
  while ((x < ConvertList.Count) and 
  (Not Found)) do begin
    Extension := ConvertIni.ReadString (
      ConvertList.Strings[x], 
      "Extension", "");
    if (UpperCase (Extension) = FileExt) then
      Found := True
    else
      x := x + 1;
  end;

  if Found then begin
    DLLName := ConvertIni.ReadString (
      ConvertList.Strings[x], 
      "ConvertDLL", "");
    {
      Загружаем DLL, получаем адрес 
      функции Convert и вызываем ее.
    }
    LibInstance := LoadLibrary (PChar(DLLName));
    if LibInstance = 0 then begin
      Application.MessageBox (
        PChar ("Can"'t load DLL 
        "+DLLName),
        "TextEdit",
        MB_ICONEXCLAMATION or MB_OK);
    end
    else begin
      Converter := GetProcAddress (LibInstance, 
      "Convert");
      if Not Assigned (Converter) then begin
        Application.MessageBox (
          PChar ("Can"'t find Convert 
          function in "+DLLName),
          "TextEdit",
          MB_ICONEXCLAMATION or MB_OK);
      end
      else begin
        if not Converter (Filename, Memo1.Lines) 
        then begin
          Application.MessageBox (
            "Error loading file",
            "TextEdit",
            MB_ICONEXCLAMATION or MB_OK);
        end;
      end;
      FreeLibrary (LibInstance);
    end;
  end
  else begin
    Application.MessageBox (
      PChar("No conversion supplied 
      for file type "+FileExt),
      "TextEdit",
      MB_ICONEXCLAMATION or MB_OK);
  end;

  ConvertList.Free;
  ConvertIni.Free;
end;

Листинг 2.9. Файл TEXTCONV.DPR

{
  TEXTCONV.DPR — DLL текстовых 
  преобразований

  Автор: Джим Мишель
  Дата последней редакции:  12/05/97
}
library textconv;

{ Важное замечание об управлении памятью в DLL: модуль ShareMem должен стоять на первом месте в секции USES библиотеки, А ТАКЖЕ в секции USES вашего проекта (команда View|Project Source), если ваша DLL экспортирует какие-либо процедуры или функции, использующие строки в качестве параметров или результатов функций. Это относится ко всем строкам, передаваемым вашей DLL или получаемым от нее — даже если эти строки вложены в записи или классы. ShareMem представляет собой интерфейсный модуль для менеджера памяти DELPHIMM.DLL, который должен использоваться вместе с вашей DLL. Чтобы обойтись без использования DELPHIMM.DLL, передавайте строковую информацию в параметрах типа PChar или ShortString. } 1

uses
  ShareMem,
  SysUtils,
  Classes,
  textc in "textc.pas";

Exports
  textc.Convert index 1 name 
  "Convert";

begin
end.

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

Листинг 2.10. Модуль TEXTC.PAS

{
  TEXTC.PAS — Модуль текстовых 
  преобразований.  Загружает
  текстовые файлы с диска.

  Автор: Джим Мишель
  Дата последней редакции:  12/05/97
}
unit textc;

interface

uses Classes;

function Convert (Filename: String; Strings: TStrings) : boolean; stdcall; export;

implementation

function Convert (Filename: String; Strings: TStrings) : boolean; stdcall;
begin
  Strings.LoadFromFile (Filename);
  Result := True;
end;

end.

Обратите внимание на примечание в начале листинга 2.9 (TEXTCONV.DPR). Оно автоматически вставляется в файл проекта при выполнении команды File|New DLL. Честно говоря, я не уверен в том, что в данном случае ссылка на модуль ShareMem так уж необходима. Я попытался запустить программу без ShareMem, и она нормально работала. Кроме того, могу выдвинуть следующий аргумент: я передаю функции Convert не сам класс, а лишь указатель на объект TStrings. Впрочем, примечание, скорее всего, относится и к указателям на классы, поэтому на всякий случай я включил ShareMem в секции uses программы и DLL. Если вам придется использовать ShareMem, не забудьте поставлять файл DELPHIMM.DLL вместе с приложением. 

Функция OpenFile из листинга 2.8 ни в коем случае не годится для коммерческой программы. Это лишь пример, который иллюстрирует общую концепцию. В коммерческом варианте ваша программа должна читать файл и (по возможности) определять его тип, а затем запрашивать у пользователя разрешение на выполнение преобразования, прежде чем начинать что-либо делать. Данный пример лишь показывает, как можно реализовать интерфейс расширения для вашего продукта. 

Совместное использование памяти вприложениях

К счастью для нас, программистов, Delphi создает DLL, по умолчанию допускающие существование нескольких экземпляров , так что хотя бы одной заботой становится меньше. Тем не менее возможность создания нескольких экземпляров еще не означает, что вам удастся легко организовать обмен информацией между процессами, использующими одну и ту же DLL. В Windows 95 и Windows NT каждый экземпляр DLL обладает собственным сегментом данных, так что вы не сможете воспользоваться простой глобальной переменной Delphi для того, чтобы передать информацию от одного работаю щего приложения другому. Для этого придется создать общий блок памяти в Windows. А для этого в свою очередь необходимо понимать, как происходит загрузка и настройка DLL в Windows и Delphi. 

Переменная DLLProc 

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

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

procedure DLLHandler (Reason: Integer); 

Параметр Reason может быть равен одной из четырех констант: DLL_PROCESS_ ATTACH, DLL_PROCESS_DETACH, DLL_THREAD_ATTACH или DLL_THREAD_DETACH

Вам придется организовать обработку сообщений DLL_PROCESS_ATTACH и вызвать CreateFileMapping, чтобы создать общий блок памяти (или получить указатель на уже имеющийся блок). Ваша DLL должна также обрабатывать сообщения DLL_PROCESS_DETACH и освобождать блок памяти, чтобы Windows могла удалить его, когда блок не будет использоваться ни одним процессом. 

В проекте SHAREME.DPR (листинг 2.11) реализован общий блок памяти. В данном примере общая память представляет собой целое число, которое увеличивается с присоединением и уменьшается с отсоединением очередного процесса. 

Листинг 2.11. Реализация общей памяти в DLL

{
  SHAREME.DPR — Пример  использования 
  общей памяти
  для организации межпроцессного 
  взаимодействия

  Автор: Джим Мишель
  Дата последней редакции:  12/05/97
}
library shareme;

uses
  Windows,
  SysUtils,
  Classes;

const
  pCounter: ^Longint = nil;

function GetProcessCount : Longint; stdcall; 
export;
begin
  Result := pCounter^;
end;

procedure MyDLLHandler (Reason: Integer);
const
  hMapObject : THandle = 0;
var
  fInit : Boolean;
begin
  case Reason of
    DLL_PROCESS_ATTACH : begin
      { Создаем именованный объект для 
      совместного доступа }
      hMapObject := CreateFileMapping (
        $FFFFFFFF,{ использовать страничный 
        файл }
        nil,{ без атрибутов безопасности }
        PAGE_READWRITE,{ доступ по чтению/
        записи }
        0,{ старшие 32 бита размера }
        sizeof (longint),{ младшие 32 бита 
        размера }
        "SharedMem"{ имя объекта }
      );
        
      { Память инициализируется первым 
      присоединенным процессом }
      fInit := (GetLastError <> 
      ERROR_ALREADY_EXISTS);

{ Получаем указатель на общую область 
памяти, 
  отображаемую на файл }
pCounter := MapViewOfFile (
  hMapObject,       { отображаемый 
  объект }
  FILE_MAP_WRITE,   { доступ по чтению/записи }
  0,                { старшие 32 бита смещения }
  0,                { младшие 32 бита смещения }
  0                 { по умолчанию:  отображение 
  на весь файл }
);
      { Инициализируем или увеличиваем счетчик }
      if (fInit) then
        pCounter^ := 1
      else
        pCounter^ := pCounter^ + 1;
    end;

    DLL_PROCESS_DETACH : begin
      { Уменьшаем счетчик }
      pCounter^ := pCounter^ - 1;

      { Разрываем связь между общей памятью
        и адресным пространством процесса }
      UnmapViewOfFile (pCounter);

      { Закрываем логический номер объекта }
      CloseHandle (hMapObject);
    end;

    (*
      Присоединение и отсоединение потоков 
      не обрабатывается

    DLL_THREAD_ATTACH :
    DLL_THREAD_DETACH :

    *)

  end;
end;

Exports
  GetProcessCount index 1 name 
  "GetProcessCount";

begin
  DLLProc := @MyDLLHandler;
  MyDLLHandler (DLL_PROCESS_ATTACH);
end.

Особое внимание следует обратить на две строки из секции инициализации DLL. Первая строка инициализирует переменную DLLProc из модуля System и заносит в нее ссылку на управляющую процедуру DLL (MyDLLHandler). Я думал, что ничего больше не потребуется, но оказалось, что при загрузке DLL почему-то не производится вызов этой процедуры с параметром DLL_PROCESS_ATTACH, поэтому такой вызов приходится организовывать в секции инициализации DLL. Видимо, в библиотеках Delphi допущена какая-то ошибка при генерации кода инициализации DLL. 

Чтобы проверить, как работает общая память, создайте форму, при инициализации которой вызывается функция DLL GetProcessCount, и выведите значение переменной-счетчика с помощью компонента TLabel. Если запустить несколько экземпляров приложения, счетчик будет увеличиваться с присоединением каждой новой копии. Если закрыть один или несколько экземпляров приложения, а потом снова открыть их, соответственно изменится и значение счетчика (то есть если запустить три экземпляра, закрыть один, а потом запустить еще один, то итоговое значение счетчика процессов будет равно 3). 

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

Продолжаем! 

Если тема вас заинтересовала, о DLL можно узнать еще много интересно го. В этой главе я привел достаточно информации, чтобы вы могли заняться самостоятельными исследованиями. Если у вас есть компакт-диски из серии Microsoft Developer's Network, проведите поиск по ключевому слову «DLL» в предметном указателе и прочитайте все, что найдете. Кроме того, почитайте о CreateFileMapping и аналогичных функциях, обращая особое внимание на отличия Windows 95 от Windows NT. С помощью DLL можно сделать много классных штук, но при этом следует проявлять осторожность. Желаю удачи!

 

Предыдущая Содержание Следующая

Используются технологии uCoz