Delphi и COM
COM-сервер, структура и использование
Модель COM предоставляет возможность создания многократно используемых компонентов, независимых от языка программирования. Такие компоненты называются COM-серверами и представляют собой исполняемые файлы (EXE) или динамические библиотеки (DLL), специальным образом оформленные для обеспечения возможности их универсального вызова из любой программы, написанной на поддерживающем COM языке программирования. При этом COM-сервер может выполняться как в адресном пространстве вызывающей программы (In-Process-сервер), так и в виде самостоятельного процесса (Out-Of-Process-сервер) или даже на другом компьютере (Distributed COM). COM автоматически разрешает вопросы, связанные с передачей параметров (Marshalling) и согласованием потоковых моделей клиента и сервера.
Далее будут рассмотрены некоторые архитектурные вопросы, знание которых необходимо для работы с COM.
COM-сервер
COM-сервер — это специальным образом оформленное и зарегистрированное приложение, которое позволяет клиентам запрашивать у себя создание реализованных в нем объектов. Сервер может быть выполнен в виде либо динамической библиотеки, либо исполняемого файла.
Сервер в виде DLL
Такой сервер всегда выполняется в адресном пространстве активизировавшего его приложения (In-Process). За счет этого, как правило, снижаются накладные расходы на вызов методов сервера. В то же время такой сервер менее надежен, поскольку его память не защищена от ошибок в вызывающем приложении. Кроме того, он не может выполняться на удаленной машине без исполнимого модуля-посредника, способного создать процесс, в который может быть загружена DLL. Примером такого модуля может служить Microsoft Transaction Server.
Сервер в виде исполнимого файла
Этот сервер представляет собой обычный исполнимый файл Windows, в котором реализована возможность создания COM-объектов по запросу других приложений. Примером такого сервера может служить пакет Microsoft Office, приложения которого являются COM-серверами.
Регистрация сервера
COM реализует механизм автоматического поиска серверов по запросу клиента. Каждый COM-объект имеет уникальный идентификатор, Class Identifier (CLSID). Windows ведет в реестре базу данных зарегистрированных объектов, индексированную при помощи CLSID. Она расположена в ветке реестра HKEY_CLASSES_ROOT\CLSID.
Для каждого сервера прописывается информация, необходимая для нахождения и загрузки его модуля. Таким образом, клиентское приложение не должно беспокоиться о поиске сервера: достаточно зарегистрировать его на компьютере — и COM автоматически найдет и загрузит нужный модуль. Кроме того, объект может зарегистрировать свое «дружественное» имя, или Programmatic Identifier (PROGID). Обычно оно формируется как комбинация имени сервера и имени объекта, например Word.Application. Это имя содержит ссылку на CLSID объекта. Когда он создается с использованием PROGID, COM просто берет связанное с ним значение CLSID и получает из него всю необходимую информацию.
Серверы в виде исполняемых файлов автоматически регистрируются при первом запуске программы на компьютере. Для регистрации серверов DLL служит программа Regsvr32, поставляемая в составе Windows, либо TRegSvr из поставки DELPHI.
Потоки и «комнаты»
Windows — многозадачная и многопоточная среда с вытесняющей многозадачностью. Применительно к COM это означает, что клиент и сервер могут оказаться в различных процессах или потоках приложения, что к серверу могут обращаться множество клиентов, причем в непредсказуемые моменты времени. Технология COM решает эту проблему при помощи концепции «комнат» (Apartments), в которых и выполняются COM-клиенты и COM-серверы. «Комнаты» бывают однопоточные (Single Threaded Apartment, STA) и многопоточные (Multiple Threaded Apartment, MTA).
STA
При создании однопоточной «комнаты» COM неявно создает окно и при вызове любого метода COM-сервера в этой «комнате» посылает данному окну сообщение при помощи функции PostMessage. Таким образом, организуется очередь вызовов методов, каждый из которых обрабатывается только после того, как будут обработаны все предшествующие вызовы. Основные достоинства однопоточной «комнаты»:
- Программист может не заботиться о синхронизации методов. Гарантируется, что до окончания выполнения текущего метода никакой другой метод объекта вызван не будет.
- Программист может не заботиться о синхронизации доступа к полям класса, реализующего объект. Поскольку одновременно может выполняться только один метод, одновременный доступ к полю из двух методов невозможен.
В то же время, если приложение создало несколько потоков, в каждом из которых имеется STA, при доступе к глобальным разделяемым данным они должны использовать синхронизацию, например при помощи критических секций.
Недостатки STA напрямую вытекают из ее реализации:
- Дополнительные (и иногда излишние) затраты на синхронизацию при вызове методов.
- Невозможность отклика на вызов метода, пока не исполнен предыдущий. Например, если в настоящее время выполняется метод, требующий одну минуту на исполнение, то до его завершения COM-объект будет недоступен.
Тем не менее STA, как правило, является наиболее подходящим выбором для реализации COM-сервера. Использовать MTA есть смысл только в том случае, если STA не подходит для конкретного сервера.
MTA
Многопоточная «комната» не реализует автоматический сервис по синхронизации и не имеет его ограничений. Внутри нее может быть создано сколько угодно потоков и объектов, причем ни один из объектов не привязан к какому-то конкретному потоку. Это означает, что любой метод объекта может быть вызван в любом из потоков в MTA. В то же самое время в другом потоке может быть вызван любой другой (либо тот же самый) метод COM-объекта по запросу другого клиента. COM автоматически ведет пул потоков внутри MTA, при вызове со стороны клиента находит свободный поток и в нем вызывает метод требуемого объекта. Таким образом, даже если выполняется метод, требующий длительного времени, то для другого клиента он может быть вызван без задержки в другом потоке. Очевидно, что COM-сервер, работающий в MTA, обладает потенциально более высокими быстродействием и доступностью для клиентов, однако он значительно сложнее в разработке, поскольку даже локальные данные объектов не защищены от одновременного доступа и требуют синхронизации.
Передача интерфейсов и параметров
Таким образом, клиент и сервер COM могут выполняться как в одной «комнате», так и в разных, расположенных в различных процессах или даже на разных компьютерах. Возникает вопрос: как же клиент может вызывать методы сервера, если они находятся (в общем случае) в другом адресном пространстве?
Эту работу берет на себя COM. Для доступа к серверу в другой «комнате» клиент должен запросить у COM создание в своей «комнате» представителя, реализующего запрошенный интерфейс. Такой представитель в терминах COM называется proxy и представляет собой объект, экспортирующий запрошенный интерфейс. Одновременно COM создает в «комнате» сервера объект-заглушку (stub), принимающий вызовы от proxy и транслирующий их в вызовы сервера. Таким образом, клиент в своей «комнате» может рассматривать proxy в качестве сервера и работать с ним так, как если бы сервер был создан в его «комнате». В то же время сервер может рассматривать stub как расположенного с ним в одной «комнате» клиента. Всю работу по организации взаимодействия proxy и stub берет на себя COM. При вызове со стороны клиента proxy получает от него параметры, упаковывает их во внутреннюю структуру и передает в «комнату» сервера. Stub получает параметры, распаковывает их и производит вызов метода сервера. Аналогично осуществляется передача параметров обратно. Этот процесс называется Marshalling. При этом «комнаты» клиента и сервера могут иметь разные потоковые модели и физически находиться где угодно. Разумеется, по сравнению с вызовом сервера в своей «комнате» такой вызов требует значительных накладных расходов, однако это единственный способ обеспечить корректную работу любых клиентов и серверов. Если необходимо избежать накладных расходов, сервер надо создавать в той же «комнате», где расположен клиент.
Для обеспечения возможности корректного создания proxy в клиентской «комнате» COM должен узнать «устройство» сервера. Сделать это можно несколькими способами:
- Реализовать на сервере интерфейс IMarshal и, при необходимости, — proxy-DLL, которая будет загружена на клиенте для реализации proxy. Подробности реализации описаны в документации COM и MSDN.
- Описать интерфейс на языке IDL (Interface Definition Language) и при помощи компилятора MIDL фирмы Microsoft сгенерировать proxy-stub-DLL.
- Сделать сервер совместимым с OLE Automation. В этом случае COM сам создаст proxy, используя описание сервера из его библиотеки типов — специального двоичного ресурса, описывающего COM-интерфейс. При этом в интерфейсе можно использовать только типы данных, совместимые с OДУ Automation.
Инициализация COM
Каким же образом клиенты и серверы COM могут создавать «комнаты» в соответствии со своими требованиями? Для этого они должны соблюдать одно правило: каждый поток, желающий использовать COM, должен создать «комнату» при помощи вызова функции CoInitializeEx. Она объявлена в модуле ActiveX.pas следующим образом:
const COINIT_MULTITHREADED = 0; // OLE calls objects on any thread. COINIT_APARTMENTTHREADED = 2; // Apartment model
function CoInitializeEx(pvReserved: Pointer; coInit: Longint): HResult; stdcall;
Параметр pvReserved зарезервирован для будущего использования и должен быть равен NIL, а параметр coInit определяет потоковую модель создаваемой комнаты. Он может принимать следующие значения:
COINIT_APARTMENTTHREADED | — для потока создается STA. Каждый поток может иметь (или не иметь) свою STA; |
COINIT_MULTITHREADED | — если в текущем процессе еще не создана MTA, создается новая MTA; если она уже создана другим потоком, поток «подключается» к ранее созданной. Иными словами, каждый процесс может иметь только одну MTA. |
Функция возвращает S_OK в случае успешного создания «комнаты».
По завершении работы с COM (или перед завершением работы) поток должен уничтожить «комнату» при помощи вызова процедуры CoUninitialize, также описанной в модуле ActiveX:
procedure CoUninitialize; stdcall;
Каждый вызов CoInitializeEx должен иметь соответствующий вызов CoUninitialize, то есть, используя COM в приложении, необходимо вызвать CoInitializeEx до первого использования функций COM и CoUninitialize перед завершением работы приложения. VCL реализует автоматическую инициализацию COM при использовании модуля ComObj. По умолчанию создается STA. При желании необходимость использовать другую потоковую модель следует установить флаг инициализации COM до оператора Application.Initialize:
program Project1; uses Forms, ComObj, ActiveX, Unit1 in 'Unit1.pas' {Form1}; {$R *.RES} begin CoInitFlags := COINIT_MULTITHREADED; Application.Initialize; Application.CreateForm(TForm1, Form1); Application.Run; end.
Если COM используется в потоке, то эти функции должны быть вызваны в методе Execute:
procedure TMyThread.Execute; begin CoInitializeEx(NIL, COINIT_MULTITHREADED); … CoUninitialize end;
Инициализация COM необходима и для вызова любых функций Windows API, связанных с COM, за исключением CoGetMalloc, CoTaskMemAlloc, CoTaskMemFree и CoTaskMemReAlloc.
Отдельного обсуждения заслуживает инициализация потоковой модели COM для сервера, расположенного в DLL. Дело в том, что DLL может быть загружена любым потоком, который уже ранее создал свою «комнату». Поэтому сервер в DLL не может сам инициализировать требуемую ему потоковую модель. Вместо этого сервер при регистрации прописывает в реестре параметр ThreadingModel, который и указывает, в какой потоковой модели способен работать данный сервер. При создании сервера COM анализирует значение этого параметра и потоковой модели «комнаты» запросившего создание сервера потока и при необходимости создает для сервера «комнату» с требуемой потоковой моделью.
Параметр ThreadingModel может принимать следующие значения:
Apartment | — сервер может работать только в STA. Если он создается из STA, то он будет создан в «комнате» вызывающего потока, если из MTA — COM автоматически создаст для него «комнату» c STA и proxy в «комнате» клиента; |
Free | — сервер может работать только в MTA. Если он создается из MTA, то он будет создан в «комнате» вызывающего потока, если из STA — COM автоматически создаст для него «комнату» c MTA и proxy в «комнате» клиента; |
Both | — сервер может работать как в STA, так и MTA. Объект всегда создается в вызывающей «комнате». |
Если этот параметр не задан, сервер имеет потоковую модель Single. В этом случае он создается в Primary STA (то есть в STA потока, который первым вызвал CoInitialize), даже если создание сервера запрошено из потока, имеющего свою отдельную STA.
Активация сервера
Для активации COM-сервера клиент должен вызвать функцию CreateComObject, описанную в модуле ComObj.pas:
function CreateComObject(const ClassID: TGUID): IUnknown;
Функция получает в качестве параметра CLSID требуемого объекта и возвращает ссылку на его интерфейс IUnknown. Далее клиент может запросить требуемый интерфейс и работать с ним:
var COMServer: IComServer; … // Создаем COM-объект и запрашиваем у него интерфейс ComServer := CreateComObject(IComServer) as IComServer; // Работаем с интерфейсом ComServer.DoSomething; // Освобождаем интерфейс ComServer := NIL;
Что же делает COM при запросе на создание сервера?
- В реестре по запрошенному CLSID ведется поиск записи регистрации сервера.
- Из этой записи получается имя исполнимого модуля сервера.
- Если это исполнимый файл, то он запускается на выполнение. Любое приложение, реализующее COM-сервер, при старте регистрирует в системе интерфейс «фабрики объектов». После запуска и регистрации COM получает ссылку на «фабрику объектов».
- Если это DLL, то она загружается в адресное пространство вызвавшего процесса и вызывается ее функция DllGetClassObject, возвращающая ссылку на реализованную в DLL «фабрику объектов».
- Фабрика объектов — это COM-сервер, реализующий интерфейс IClassFactory. Ключевым методом этого интерфейса является метод CreateInstance, который и создает экземпляр требуемого объекта.
- COM вызывает метод CreateInstance и передает полученный интерфейс клиенту.
По завершении работы с COM-объектом клиент освобождает ссылку на него (что приводит к вызову метода Release). В этот момент COM-сервер проверяет, есть ли еще ссылки на созданные им объекты. Если все объекты освобождены, то COM-сервер завершает свою работу. В случае если он реализован в виде DLL, он должен экспортировать функцию DllCanUnloadNow, которая вызывается COM по таймеру или при вызове функции API CoFreeUnusedLibraries. Если все объекты из этой DLL освобождены, она выгружается из памяти.
Вся работа по созданию и регистрации «фабрики объектов» и экспорту соответствующих функций из DLL в Delphi уже реализована в составе стандартных библиотек, и создание COM-сервера в действительности является очень простой задачей.
Создание COM-сервера
Для создания COM-сервера Delphi предоставляет широкий набор мастеров, автоматизирующих выполнение рутинных задач и позволяющих программисту сконцентрироваться на реализации функциональности. Мастера доступны при помощи команды меню File->New, на закладке ActiveX.
Чтобы сделать COM-сервером EXE-файл, необходимо просто добавить в него модуль с COM-объектом. Для создания COM-сервера в виде DLL потребуется сначала создать библиотеку, оформленную с учетом требований COM. Это делается при помощи мастера ActiveX Library. При его выборе будет создан новый проект, реализующий DLL, и сгенерирован следующий код:
library Project1; uses ComServ; exports DllGetClassObject, DllCanUnloadNow, DllRegisterServer, DllUnregisterServer; {$R *.RES} begin end.
Созданная DLL экспортирует функции, необходимые для работы COM, при этом можно не отвлекаться на рутинную работу и сразу приступить к реализации COM-сервера.
Для этого выберите мастер «COM-объект».
От заполнения полей этой формы зависит реализация создаваемого COM-объекта:
Для поддержки OleAutomation-маршалинга необходимо:
- чтобы сервер был унаследован от TTypedComObject (реализация IDispatch не обязательна);
- все методы интерфейса были объявлены как safecall. Если вы создаете интерфейс, унаследованный от IUnknown, то по умолчанию все его методы объявляются как stdcall. Чтобы создать safecall-методы, необходимо в диалоге Tools>Environment Options на закладке Type Library установить переключатель Safecall function mapping в значение All v-table interfaces.
Сервер без библиотеки типов
Такой сервер, если он не реализует интерфейс IMarshall, может работать лишь в одной «комнате» с клиентом, поэтому его следует использовать только для In-Process-серверов с потоковой моделью, идентичной клиенту.
При создании сервера, не включающего библиотеку типов, необходимо указать мастеру реализуемые им интерфейсы. Укажем имя интерфейса ITest. По завершении работы мастера будет создан следующий модуль:
unit Unit1; interface uses Windows, ActiveX, Classes, ComObj; type TTest = class(TComObject, ITest) protected end; const Class_Test: TGUID = '{1302FB00-703F-11D4-84DD-825B45DBA617}'; implementation uses ComServ; initialization TComObjectFactory.Create(ComServer, TTest, Class_Test, 'Test', '', ciMultiInstance, tmApartment); end.
Внимание! COM-сервер, который может использоваться различными клиентами (а не только в рамках конкретного проекта, в котором спецификации клиентов жестко заданы), не рекомендуется создавать без поддержки маршалинга данных, поскольку в этом случае невозможно обеспечить гарантированное нахождение его в одной «комнате» с клиентом. Если вы все же делаете такой сервер, в документации на него необходимо отразить требуемые спецификации клиента. |
Рассмотрим сгенерированный код подробнее. Особый интерес представляет секция Initialization. В ней создается экземпляр «фабрики объектов» — COM-сервера, реализующего интерфейс IClassFactory2. К нему COM будет обращаться для создания экземпляра объекта Test. VCL автоматически выполняет всю рутинную работу по взаимодействию с COM.
Для реализации сервера требуется написать интерфейсный модуль с описанием реализуемого интерфейса. Кроме того, вынесем в него описание константы Class_Test и добавим его в строку uses модуля Unit1:
unit TestInterface; interface const Class_Test: TGUID = '{1302FB00-703F-11D4-84DD-825B45DBA617}'; type ITest = interface ['{1C986802-6D6D-11D4-84DD-996A491CE716}'] procedure ShowIt(S: String); end; implementation end.
Этот модуль содержит всю необходимую информацию для работы сервера и должен использоваться при компиляции клиента.
Дополним код COM-объекта реализацией методов реализуемого интерфейса:
unit Unit1; interface uses Windows, ActiveX, Classes, ComObj, TestInterface; type TTest = class(TComObject, ITest) protected procedure ShowIt(S: String); end; implementation uses ComServ; { TTest } procedure TTest.ShowIt(S: String); begin MessageBox(0, PChar(S), NIL, 0); end; initialization TComObjectFactory.Create(ComServer, TTest, Class_Test, 'Test', '', ciMultiInstance, tmApartment); end.
Откомпилировав проект, мы получим файл Project1.dll.
Последним шагом является регистрация COM-сервера.
Введем в командной строке «regsvr32 project1.dll».
Если все было проделано правильно, на экране должно появиться сообщение об успешной регистрации: «DllRegisterServer in Project1.dll succeeded».
Теперь можно приступать к написанию клиента. Для этого создадим новый проект, добавим в модуль с его главной формой строку uses TestInterface и напишем следующий код:
uses TestInterface, ComObj; procedure TForm1.Button1Click(Sender: TObject); var Test: ITest; begin Test := CreateComObject(Class_Test) as ITest; Test.ShowIt('Hi'); end;
Как видно из этого примера, создание и использование COM-сервера не сложнее, чем работа с обычными классами Delphi. Сервер без библиотеки типов является хорошим выбором для реализации COM-серверов, используемых внутри проекта, поскольку для его работы нужен интерфейсный модуль. При передаче сервера другим разработчикам вам придется передать им этот модуль и при необходимости перевести его на другой язык (например, С).
Сервер с библиотекой типов
Библиотека типов — это специальный двоичный ресурс, описывающий интерфейсы и методы, реализуемые COM-сервером. Кроме наличия библиотеки типов сервер должен поддерживать интерфейс IProvideClassInfo. В Delphi такой сервер реализуется путем наследования его от TTypedComObject. Для этого оставьте флажок Include Type Library в мастере создания COM-объекта включенным.
Создадим COM-сервер в виде EXE (разумеется, он может быть также создан и виде DLL).
Сначала создадим новый проект — File-New Application, а затем добавим в него COM-объект.
Если не отключать флажок Include Type Library, то мастер создаст уже не один, а два модуля. Первый из них напоминает созданный ранее.
unit Unit1; interface uses Windows, ActiveX, Classes, ComObj, Project1_TLB, StdVcl; type TTest1 = class(TTypedComObject, ITest1) protected {Declare ITest1 methods here} end; implementation uses ComServ; initialization TTypedComObjectFactory.Create(ComServer, TTest1, Class_Test1, ciMultiInstance, tmApartment); end.
Наиболее интересна строка: uses … Project1_TLB. Это автоматически сгенерированный интерфейсный модуль к нашему COM-объекту (аналогично TestInterface.pas в предыдущем примере). Он содержит описание всех необходимых для работы с сервером интерфейсов. В отличие от предыдущего примера, вам не придется редактировать его вручную. Для этого Delphi откроет редактор библиотеки типов:
Это специализированный редактор для описания интерфейсов COM-объектов. Вы должны описать все требуемые интерфейсы, методы и т.п. в этом редакторе, после чего можно нажать кнопку «Обновить» — и изменения будут автоматически внесены во все требуемые модули. Вам останется лишь дописать реализацию методов.
Добавим описание нового метода. Для этого щелкнем правой кнопкой мыши на интерфейсе ITest и выберем из контекстного меню опцию New->Method. Введем имя метода — ShowIt.
На закладке Parameters зададим параметр S и тип BSTR. После этого нажмем кнопку «обновить» и посмотрим, что произошло с исходными текстами нашей программы. В модуле Project1_TLB в описании интерфейса ITest1 появился метод ShowIt:
ITest1 = interface(IUnknown) ['{1302FB06-703F-11D4-84DD-825B45DBA617}'] function ShowIt(const S: WideString): HResult; stdcall; end;
А в модуле Unit1:
type TTest1 = class(TTypedComObject, ITest1) protected function ShowIt(const S: WideString): HResult; stdcall; end; implementation uses ComServ; function TTest1.ShowIt(const S: WideString): HResult; begin end;
Нам остается лишь написать реализацию метода:
function TTest1.ShowIt(const S: WideString): HResult; begin MessageBoxW(0, PWideChar(S), NIL, 0) Result := S_OK; // Стандартный код успешного завершения end;
Для регистрации сервера достаточно один раз запустить его на компьютере клиента.
Перейдем к написанию приложения-клиента. При наличии модуля Project_TLB оно ничем не будет отличаться от предыдущего примера. Более интересен случай, когда мы имеем только исполняемый файл с сервером. Зарегистрируем этот сервер и выберем в меню Delphi IDE команду Project -> Import Type Library.
В открывшемся окне найдем строку с описанием библиотеки типов требуемого сервера.
Если включен флажок Generate Component Wrappers, то в импортированный модуль будет добавлен код для создания компонента Delphi, который можно поместить на форму — и он автоматически создаст требуемый COM-сервер и позволит обращаться к его методам. В противном случае будет сгенерирован модуль, содержащий описание всех имеющихся в библиотеке типов интерфейсов.
Далее необходимо определить, что вы собираетесь сделать с выбранной библиотекой:
Таким образом, для распространения и использования сервера не требуется ничего, кроме его исполнимого модуля. Но это не самое главное. Гораздо более важно, что вы можете импортировать и использовать в своей программе любой из имеющихся на компьютере COM-серверов. Естественно, что при передаче своей программы клиенту вы должны установить на его компьютере соответствующий COM-сервер.
Для примера используем в своем приложении процессор регулярных выражений VBScript. Импортируем библиотеку типов Microsoft VBScript Regular Expressions.
При этом будет создан файл VBScript_RegExp_TLB.pas.
Создадим форму и добавим следующий код для проверки вхождения текста, содержащегося в компоненте Edit1, в текст, содержащийся в компоненте Edit2:
uses VBScript_RegExp_TLB; procedure TForm1.Button1Click(Sender: TObject); var RE: IRegExp; begin RE := CoRegExp.Create; RE.Pattern := Edit1.Text; if RE.Test(Edit2.Text) then Caption := 'TRUE' else Caption := 'FALSE'; end;
Это все! Мы получили в своем приложении поддержку регулярных выражений — такую же, как и та, что включена в скриптовые языки Microsoft (VBScript и JScript).
Создание Plug-In в виде COM-сервера
Попробуем теперь реализовать Plug-In к своей программе в виде COM-сервера и сравним код, полученный в этом случае, с кодом, полученным при «ручном» программировании. Вначале создадим модуль с описанием интерфейсов:
Unit PluginInterface; interface const Class_TAPI: TGUID = '{A132D1A1-721C-11D4-84DD-E2DEF6359A17}';
type IAPI = interface ['{64CFF1E0-61A3-11D4-84DD-B18D6F94141F}'] procedure ShowMessage(const S: String); end;
ILoadFilter = interface ['{64CFF1E1-61A3-11D4-84DD-B18D6F94141F}'] procedure Init(const FileName: String); function GetNextLine(var S: String): Boolean; end; implementation end.
Обратите внимание, что метод ILoadFilter.Init больше не получает ссылки на внутренний API программы — он будет реализован в виде COM-объекта.
Создадим DLL c COM-сервером, реализующим ILoadFilter. Для этого создадим новую ActiveX-библиотекуи добавим в нее COM-объект TLoadFilter. Установим ThreadingModel в Single, поскольку использование сервера в потоках не предусмотрено. После этого реализуем методы интерфейса ILoadFilter:
unit Unit3; interface uses Windows, ActiveX, Classes, ComObj, PluginInterface; type TLoadFilter = class(TComObject, ILoadFilter) private FAPI: IAPI; F: TextFile; Lines: Integer; InitSuccess: Boolean; protected procedure Init(const FileName: String); function GetNextLine(var S: String): Boolean; public destructor Destroy; override; end;
const Class_LoadFilter: TGUID = '{A132D1A2-721C-11D4-84DD-E2DEF6359A17}'; implementation uses ComServ, SysUtils;
Деструктор и метод GetNextLine аналогичны предыдущему примеру:
destructor TLoadFilter.Destroy; begin if InitSuccess then CloseFile(F); inherited; end;
function TLoadFilter.GetNextLine(var S: String): Boolean; begin if InitSuccess then begin Inc(Lines); Result := not Eof(F); if Result then begin Readln(F, S); FAPI.ShowMessage('Загружено ' + IntToStr(Lines) + ' строк.'); end; end else Result := FALSE; end;
Метод Init имеет существенное различие — теперь ссылку на внутренний API программы мы получаем при помощи COM. Это освобождает нас от необходимости передавать ссылку в модуль расширения.
procedure TLoadFilter.Init(const FileName: String); begin FAPI := CreateComObject(Class_TAPI) as IAPI; {$I-} AssignFile(F, FileName); Reset(F); {$I+} InitSuccess := IOResult = 0; if not InitSuccess then FAPI.ShowMessage('Ошибка инициализации загрузки'); end;
В конце модуля находится код, автоматически сгенерированный Delphi для создания фабрики объектов:
initialization TComObjectFactory.Create(ComServer, TLoadFilter, Class_LoadFilter, 'LoadFilter', '', ciMultiInstance, tmSingle); end.
Компилируем DLL и регистрируем ее при помощи regsvr32.
Поскольку программа может поддерживать множество различных фильтров, организуем их подключение через INI-файл следующего вида:
[Filters] TXT={A132D1A2-721C-11D4-84DD-E2DEF6359A17}
Параметром строки служит CLSID сервера, реализующего фильтр. В нашем случае это содержание константы Class_LoadFilter. Для подключения дополнительных фильтров необходимо создать DLL с сервером, реализующим ILoadFilter, зарегистрировать ее в системе и добавить CLSID сервера в INI-файл.
Теперь можно приступить к написанию программы-клиента. Она аналогична используемой в предыдущем примере. Добавим в нее COM-сервер, реализующий внутренний API.
За исключением кода, сгенерированного COM, этот объект полностью аналогичен объекту, приведенному ранее. Константу Class_TAPI вынесем в модуль PluginInterface, чтобы сделать ее доступной для модулей расширения:
unit Unit2; interface uses Windows, ActiveX, Classes, ComObj, PluginInterface; type TTAPI = class(TComObject, IAPI) protected procedure ShowMessage(const S: String); end;
implementation uses Forms, ComServ, Unit1; { TTAPI } procedure TTAPI.ShowMessage(const S: String); begin (Application.MainForm as TForm1).StatusBar1.SimpleText := S; end; initialization TComObjectFactory.Create(ComServer, TTAPI, Class_TAPI, 'TAPI', '', ciMultiInstance, tmSingle); end.
Теперь все готово к реализации функциональности клиента. В целях экономии места приведем лишь метод LoadData:
procedure TForm1.LoadData(FileName: String); var PlugInName: String; Filter: ILoadFilter; S, Ext: String; begin Memo1.Lines.Clear; Memo1.Lines.BeginUpdate; try Ext := ExtractFileExt(FileName); Delete(Ext, 1, 1); with TIniFile.Create(ExtractFilePath(ParamStr(0)) + 'plugins.ini') do try PlugInName := ReadString('Filters', Ext, ''); finally Free; end; Filter := CreateComObject(StringToGUID(PlugInName)) as ILoadFilter; Filter.Init(FileName); while Filter.GetNextLine(S) do Memo1.Lines.Add(S); finally Memo1.Lines.EndUpdate; end; end;
Очевидно, что код метода стал гораздо более коротким и читабельным. COM взял на себя всю черновую работу по поиску, загрузке и и выгрузке DLL, поиску и созданию объектов.
Внимание! Поскольку в EXE и DLL используются длинные строки, не забудьте включить в список uses обоих проектов модуль ShareMem. |
Автоматическая регистрация серверов из своей программы
Удобно в своей программе автоматически регистрировать все необходимые серверы. Это можно сделать при помощи следующей процедуры:
procedure CheckComServerInstalled(const CLSID: TGUID; const DllName: String); var Size: Integer; DllHandle: THandle; FileName: String; begin Size := MAX_PATH; SetLength(FileName, Size); try if RegQueryValue(HKEY_CLASSES_ROOT, PChar(Format('CLSID\%s\InProcServer32', [GUIDToString(CLSID)])), PChar(FileName), Size) = ERROR_SUCCESS then begin SetLength(FileName, Size); DllHandle := LoadLibrary(PChar(FileName)); FreeLibrary(DllHandle); if DllHandle = 0 then begin RegDeleteKey(HKEY_CLASSES_ROOT, PChar(Format('CLSID\%s',[GUIDToString(CLSID)]))); RegisterComServer(DllName); end; end else begin RegisterComServer(DllName); end; except raise Exception.CreateFmt('Не могу зарегистрировать %s.', [DllName]); end; end;
В процедуре осуществляется дополнительная проверка наличия на диске файла с зарегистрированным сервером. Если файл не найден по указанному в реестре месту, данные о регистрации удаляются и предпринимается попытка зарегистрировать сервер заново. Такая проверка очень полезна при переносе DLL с сервером в другую папку на диске.