задание третья тема (COM-RPC)

advertisement
Содержание
Основные понятия и определения ...................................................................... 3
Технология COM как спецификация и реализация .......................................... 4
Расширения COM (COM extensions) .................................................................. 5
Базовый интерфейс Iunknown ............................................................................. 8
Указатели на интерфейсы COM объекта ........................................................... 8
COM серверы ........................................................................................................ 9
Фабрика классов и класс CoClass ....................................................................... 9
Последовательность событий при обращении клиента к серверу COM ......... 11
Сервер в клиентском процессе (In-process server) .......................................... 14
Локальный сервер (local server) ........................................................................ 14
Удаленный сервер (remote server) .................................................................... 15
Маршалинг .......................................................................................................... 16
Проектирование COM объекта ......................................................................... 18
Разработка сервера ............................................................................................. 29
Разработка клиентского приложения ............................................................... 30
Использование MS Word путем импорта библиотеки типов ........................ 36
Использование MS Word путем включения в проект компонента Delphi ... 39
Разработка клиентских приложений для MS Office как контролеров
автоматизации .................................................................................................... 41
Простые СОМ-клиенты и СОМ-серверы ........................................................... 48
Макросы STDMETHOD и STDMETHODIMP ................................................ 49
Проект сервера ................................................................................................... 51
Приложение клиента.......................................................................................... 62
Технология объектно-ориентированного подхода CORBA ............................. 66
Интерфейс ........................................................................................................... 68
Сервант ................................................................................................................ 69
Объектная ссылка............................................................................................... 70
2
Введение в COM технологии
Основные понятия и определения
COM (Component Object Model) – модель компонентных объектов
Microsoft. Стандартный механизм, включающий интерфейсы, с помощью
которых одни объекты предоставляют свои сервисы другим. Является
основой многих объектных технологий, в том числе OLE и ActiveX. Другой
перевод: многокомпонентная модель объектов.
DCOM (Distributed Component Object Model) – распределенная
модель компонентных объектов. Расширение модели COM фирмы Microsoft,
ориентированное на поддержку и интеграцию распределенных объектных
приложений, функционирующих в сети.
COM представляет собой основанную на объектах клиент-серверную
модель, разработанную Microsoft для обеспечения взаимодействия между
компонентами программного обеспечения и приложениями. Microsoft
расширила эту технологию добавлением элементов ActiveX, которые
представляют результат развития технологий OLE и OCX. OCX (OLE Custom
eXtension) – это программируемые компоненты-приложения с интерфейсом
на базе OLE, позволяющим легко включать их в другие приложения. С 1996
года они стали называться ActiveX.
Сейчас Microsoft предлагает использовать термин Active technologies
вместо ActiveX, включая в новые технологии такие составляющие:
· Active Documents (активные документы);
· ActiveX (управляющие элементы);
· Active Scripting controls;
· Automation (автоматизация, прежде известная как OLE Automation).
Ключевым
обеспечивает
аспектом
COM
связь
между
посредством интерфейсов.
Именно
является
то,
что
клиентами
интерфейс
эта
технология
и
предоставляет
серверами
клиенту
способ "узнать" у сервера, какие именно возможности он поддерживает на
3
этапе выполнения. Для расширения возможностей сервера необходимо
просто добавить новый интерфейс к существующим.
Delphi предоставляет программисту мастеров (wizards) и классы,
которые облегчают разработку приложений, основанных на технологиях
COM. Мастера позволяют создавать:
· простые COM–совместимые классы для использования в одном
приложении;
· полновесные серверы COM;
·
серверы
автоматизации
(Automation
servers)
и
контроллеры
автоматизации (Automation controller);
· управляющие элементы ActiveX;
· активные формы ActiveForms.
Технология COM как спецификация и реализация
COM является одновременно и спецификацией и реализацией.
Спецификация
COM
определяет
правила
создания
объектов
и
их
взаимодействия, способ связи между объектами. В соответствии со
спецификацией объекты COM могут быть написаны на различных языках,
выполняться
разнообразных
в
адресном
пространстве
платформах. До
тех
различных
пор, пока
процессов
объекты
и
на
полностью
соответствуют спецификации, они могут взаимодействовать. Это позволяет
объединять унаследованный код как компонент с новыми компонентами,
разработанными в любом из объектно-ориентированных языков.
COM как реализация представляет собой библиотеку (файлы OLE32.dll
OLEAut32.dll),
которая
предоставляет
ряд
основных
служб,
поддерживающих описанные спецификации. Библиотека COM содержит
набор
стандартных
интерфейсов,
определяющих
основную
функциональность объектов COM, и небольшой набор функций API,
разработанных для целей создания и управления объектами COM.
Интерфейсы объектов Delphi, как и язык Object Pascal, соответствуют
COM спецификации. Реализация COM в Delphi называется DAX (Delphi
4
ActiveX framework – базовая структура (каркас) элементов ActiveX Delphi).
Основная часть реализации этой структуры находится в модуле AxCtrls.
Когда
программист
использует
мастеров
Delphi
или
объекты
библиотеки VCL в своем приложении, он использует реализацию COM
спецификаций. Кроме того, Delphi предоставляет ряд упаковщиков (wrapper)
для таких служб COM, которые не реализуются непосредственно, например,
активные документы (Active Documents). Эти упаковщики включены в
модуль ComObj
Расширения COM (COM extensions)
Так как COM является развивающейся технологией, она может быть
расширена за рамки базисных служб. COM является основой для других
технологий, таких как автоматизация (Automation – вместо прежнего термина
OLE Automation), элементы ActiveX и активные документы (Active
Documents).
Кроме того, в настоящее время возможно создание таких объектов
COM, которые могут взаимодействовать с Microsoft Transaction Server (MTS).
MTS – это система обработки транзакций, предназначенная для построения,
развертывания и управления большими Intranet и Internet приложениямисерверами. Хотя MTS архитектурно и не является частью COM, она
разработана для расширения возможностей COM в большой, распределенной
среде.
Delphi снабжает программиста мастерами, облегчающими разработку
приложений,
которые
объединяют
упомянутые
выше
технологии
–
Automation, ActiveX, Active Documents и MTS.
5
Составляющие приложений COM
При разработке приложений, использующих технологию COM,
используются структурные элементы, приведенные в таблице.
Элемент
Назначение
Средство, с помощью которого объект COM предоставляет свои
COM
функциональные возможности (службы) для внешних клиентов.
Interface Объект COM снабжает интерфейсом каждый набор методов и
свойств. Любой объект COM имеет один или более интерфейсов
COM
Некоторый модуль (EXE, DLL или OCX), который содержит код
server
для объекта COM
Программный код, который получает требуемые услуги от
COM
client
сервера через интерфейс(ы) объекта COM. Клиент знает, что он
хочет получить от сервера, но не знает как его запрос выполняется
внутри сервера. В большинстве случаев клиент реализуется как
Automation controller (то же, что и ActiveX-клиент)
Библиотека типов, которая содержит описание COM объекта, его
интерфейсов и методов, а также их GUID идентификаторы.
Информация из библиотеки записывается в системный реестр и
Type
Library
используется клиентским приложением. Delphi предоставляет
специальный
редактор
библиотеки
типов,
а
содержимое
библиотеки типов представляет в виде обычного модуля на языке
Object Pascal
Class
Factory
Фабрика классов, экземпляр объекта которой создает COM
объект. Сам объект фабрики классов создается COM сервером при
запросе клиентским приложением первого интерфейса
Интерфейсы COM
Клиент взаимодействует с объектом COM посредством интерфейса,
который представляет собой набор функционально или семантически
связанных подпрограмм, с помощью которых обеспечивается связь клиента с
6
провайдером сервисов (сервером). Стандартный способ изображения
интерфейса приведен на рис.1.
Рисунок 1. Стандартный способ изображения интерфейса
Например, каждый COM объект имеет базовый интерфейс IUnknown,
который сообщает, какие интерфейсы объекта доступны клиенту. Любой
интерфейс сообщает клиенту, какие возможности он предоставляет.
Ключевыми
аспектами
интерфейсов
объектов
COM
являются
следующие:
 будучи однажды опубликованным, интерфейс не должен изменяться ни
при
каких
обстоятельствах.
При
необходимости
расширение
функциональности должно быть выполнено за счет добавления других
интерфейсов;
 по соглашению, имя интерфейса должен начинаться с заглавной буквы
I, например, IMalloc или IPersist;
 интерфейс имеет гарантированный уникальный идентификатор GUID
(Globally Unique Identifier), который представляет собой 128-битное
случайное число. Эти идентификаторы называют Interface Identifiers
(IIDs).
Использование
таких
идентификаторов
ограничивает
возможность конфликтов между разными версиями или продуктами;
 интерфейсы обладают языковой независимостью. Для реализации
интерфейса может быть использован любой язык программирования,
который поддерживает указатели и структуры и позволяет вызвать
функцию по указателю на нее;
 сами по себе интерфейсы не являются объектами, а предоставляют
способ получения доступ к объекту, а клиент получает доступ к
данным объекта посредством вызова функций;
7
 любой
интерфейс
является
наследником
базового
интерфейса IUnknown;
 обращения к интерфейсам могут перенаправляться между потоками,
процессами и сетевыми компьютерами невидимо для клиента.
Базовый интерфейс Iunknown
Этот интерфейс, который должны поддерживать все COM объекты,
включает следующие подпрограммы:
 QueryInterface;
 AddRef;
 Release.
Метод QueryInterface, объявленный как
Function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
возвращает клиенту указатель на запрошенный интерфейс IID. С
помощью полученного указателя
клиент может вызвать любой из
реализованных методов интерфейса. В качестве IID клиент может указать
тип класса интерфейс – в этом случае компилятор самостоятельно извлекает
соответствующий GUID.
Методы AddRef и Release используются для того, чтобы объект COM
мог самостоятельно отслеживать продолжительность своего существования.
Эти методы просто изменяют число ссылок на объект. Когда число ссылок на
объект становится равным нулю, объект удаляется из памяти COM сервером.
Указатели на интерфейсы COM объекта
Указатель на интерфейс является 32-битным указателем, который
ссылается на указатель на таблицу vtable (рис 2.). Эта таблица является
массивом указателей, каждый из которых, в свою очередь, указывает на
реализацию метода. Таблица vtable является общей для всех экземпляров
объекта, но для каждого из экземпляров создается свой набор данных.
8
Рисунок 2
COM серверы
Сервер COM является приложением или библиотекой, которая
предоставляет сервис клиентскому приложению (или библиотеке). Сервер
включает по крайней мере один объект COM, который в свою очередь
представляет собой совокупность методов и свойств. Клиент не обязан знать,
где в памяти располагаются объекты COM.
Когда клиент запрашивает сервис у объекта COM, он (клиент) должен
передать идентификатор класса CLSID (class identifier). Идентификатор
класса CLSID создается на основе GUID интерфейса объекта COM.
По идентификатору класса CLSID COM определяет соответствующий
сервер, загружает его в память и сервер создает экземпляр объекта COM.
Экземпляры объектов COM создает фабрика классов (class factory), к которой
обращается сервер. Фабрика классов имеет свой интерфейс IClassFactory.
Фабрика классов и класс CoClass
COM объект является экземпляром класса CoClass, в котором
реализованы один или более интерфейсов COM. Объект COM обеспечивает
те сервисы, которые определены в интерфейсах класса CoClass.
Экземпляры класса CoClass создаются специальным типом объекта,
который называется фабрикой класса. Когда клиент обращается к COM
объекту, фабрика класса создает экземпляр объекта и регистрирует
экземпляр объекта для этого конкретного клиента. Если в это время другой
клиент обращается к объекту, фабрика классов для него также создает
(другой) экземпляр объекта.
9
Любой класс CoClass должен иметь фабрику классов и идентификатор
класса CLSID, так что экземпляр COM объекта этого класса может быть
создан извне, т.е. из другого модуля. Благодаря наличию у классов CoClass
уникального идентификатора CLSID, они могут быть обновлены в любое
время, как только для класса разработан новый интерфейс. Новый интерфейс
может использовать модифицированные или новые методы и это не
оказывает никакого влияния на прежние версии. В случае использования
обычных библиотек DLL подобная ситуация прежде была типичной
проблемой.
Мастера Delphi избавляют программиста от необходимости заниматься
созданием объектов, которые обеспечивают функционирование фабрики
классов.
10
Последовательность событий при обращении клиента к
серверу COM
Шаг 1. Клиентское приложение запрашивает интерфейс путем вызова
конструктора сокласса. Сокласс является производным от класса TObject и
содержит
только
два
конструктора
и
больше
никаких
методов.
Конструктор Create используется для внутрипроцессных или локальных
серверов, а CreateRemote – для серверов, размещенных на удаленных
компьютерах. Любой из этих конструкторов должен создать экземпляр
объекта и возвратить указатель на первый интерфейс (интерфейс по
умолчанию), предоставляемый COM объектом. В Delphi конструктор
сокласса вызывает метод CreateComObject (для локального сервера) или
CreateRemoteComObject (для удаленного сервера). В свою очередь метод
CreateComObject
вызывает
функцию
Win API CoCreateInstance
(или
CoCreateInstanceEx в случае удаленного сервера). Вот реализация метода
CreateComObject в модуле ComObject:
Function CreateComObject(const ClassID: TGUID): IUnknown;
Begin
OleCheck(CoCreateInstance(ClassID, nil, CLSCTX_INPROC_SERVER or
CLSCTX_LOCAL_SERVER, IUnknown, Result));
End;
Функция-оболочка
OleCheck
предназначена
для
генерации
исключительной ситуации в случае ошибки – и только.
Главный параметр обсуждаемых функций – это идентификатор
сокласса ClassID, который идентифицирует в реестре Windows библиотеку
(или приложение), содержащую реализацию сервера. Вот содержимое ветви
реестра
HKEY_CLASSES_ROOTCLSID
для
зарегистрированного
внутрипроцессного сервера:
[{40BC0668-D946-11D5-BE01-ABCDCE4D71F9}] ="SmpCOM Object"
InprocServer32="D:WORKDIRSIMPLECOMSIMPLECOM.DLL"
11
InprocServer32"ThreadingModel"="Apartment"
ProgID="SimpleCOM.SmpCOM"
Version="1.0"
TypeLib="{40BC0665-D946-11D5-BE01-ABCDCE4D71F9}"
Первый GUID – это и есть идентификатор сокласса. Соответствующую
константу (типа TGUID) Delphi по умолчанию именует с префиксом
CLASS_, например, CLASS_SmpCOM.
Шаг 2. Функция CoCreateInstance иницирует библиотеку COM, которая
по идентификатору CLSID отыскивает в реестре библиотеку .dll (или
приложение) и загружает ее в память.
Шаг 3. Создается экземпляр объекта фабрика класса. Для случая
реализации сервера в виде библиотеки в ее секции инициализации
содержится вызов конструктора класса TTypedComObjectFactory (для сервера
с библиотекой типов) или TComObjectFactory (для сервера без библиотеки
типов). Если сервер реализован в виде библиотеки, то вызов этого
конструктора делается в секции инициализации библиотеки. Конструктор
класса TTypedComObjectFactory в конечном итоге вызвает унаследованный
конструктор
класса
TComObjectFactory.
Параметры
конструкторов
определяют вид COM сервера и модель его функционирования.
Шаг 4. Конструктор сокласса создает COM объект и возвращает
указатель на запрошенный интерфейс. Теперь клиентское приложение может
вызывать методы интерфейса или, с помощью метода QueryInterface, может
получить указатель на любой другой интерфейс, поддерживаемый объектом,
по его идентификатору GUID.
Шаг 5. Объект COM самостоятельно ведет подсчет ссылок на его
интерфейсы. Когда число этих ссылок равно нулю, COM объект
уничтожается самостоятельно. Библиотека (или приложение) выгружается из
памяти службами COM с помощью функции Win API CoFreeUnusedLibraries.
Эта функция выгружает библиотеку с COM объектом в том случае, если
функция
DllCanUnloadNow
возвращает
значение
S_OK.
Реализация
12
библиотеки сервера в соответствии со спецификацией COM должна
экспортировать, в общем случае, всего 4 функции: DllGetClassObject,
DllCanUnloadNow, DllRegisterServer и DllUnregisterServer.
13
Способы реализации СОМ серверов
При работе с объектами COM клиент не знает, где находится объект.
Он просто обращается к объекту посредством его интерфейса. Далее
библиотека COM выполняет все необходимые шаги для удовлетворения
вызова клиента.
Эти шаги зависят от конкретного местонахождения объекта COM: в
том же процессе, что и клиент, в другом процессе на машине клиента или на
другой машине в сети. В зависимости от этого различают три вида серверов
COM.
Сервер в клиентском процессе (In-process server)
Это библиотека DLL, которая выполняется в адресном пространстве
процесса клиента. Например, элемент ActiveX, внедренный в Web страницу,
выполняется в Internet Explorer или другом браузере. Это значит, что объект
ActiveX загружается на машину клиента и выполняется в том же процессе,
что и Web браузер. Клиент обращается к объекту COM путем прямого
вызова интерфейса COM.
Рисунок 3. Сервер в клиентском процессе
Локальный сервер (local server)
Он представляет собой другое приложение (файл *.exe), которое
выполняется в другом адресном пространстве, но на том же компьютере, что
и клиентское приложение. Например, обработка таблицы Excell, внедренной
14
в документ Word, выполняется приложением Excell. Локальный сервер
связывается с клиентом посредством COM библиотек.
Рисунок 4. Локальный сервер
Когда объект COM принадлежит другому процессу на том же
компьютере, что и клиентское приложение, или вовсе на другом компьютере
(в сети), COM использует так называемого "заместителя" (proxy) для того,
чтобы инициировать удаленный вызов процедуры (remote procedure call –
RPC). Так как заместитель находится в клиентском процессе, то с точки
зрения клиента обращения к интерфейсам выглядят так же, как и для случая
размещения сервера в клиентском процессе. Заместитель перехватывает
вызовы клиента и отправляет их туда, где находится сервер. Механизм,
который позволяет клиенту получить доступ к объекту, находящемуся в
другом процессе или на другом компьютере (невидимо для клиента),
называется маршалингом или маршализацией.
Удаленный сервер (remote server)
Он представляет собой библиотеку (DLL или OCX) или другое
приложение, которые выполняются на другом компьютере, а не на машине
клиента. Например, клиентское приложение, использующее базу данных,
связывается с приложением, выполняемым на другом компьютере в сети. В
этом случае удаленный сервер использует интерфейсы DCOM.
15
Рисунок 5. Удаленный сервер
Различие между локальным и удаленным сервером состоит в
применяемом способе (и инструментальных средствах) взаимодействия
клиента и сервера: в первом случае используется COM, а во втором – DCOM.
Если сервер реализован как библиотека (а библиотека не может
выполняться как самостоятельное приложение), то COM создает специальное
приложение-суррогат
(surrogate),
в
рамках
которого
и
выполняется
библиотека.
Маршалинг
Механизм маршалинга позволяет клиентскому приложению делать
вызовы методов интерфейса для объекта COM, который находится в другом
процессе или на другом компьютере.
При любом вызове функций посредством интерфейса клиентское
приложение помещает фактические параметры функции в стек и выполняет
ее вызов. Если функция не находится в клиентском процессе, то вызов
передается заместителю. Заместитель упаковывает параметры в пакет и
передает их удаленному объекту. Заглушка COM объекта распаковывает
пакет, помещает аргументы в стек и вызывает объект COM. Таким образом
объект COM обрабатывает запрос клиента в своем собственном адресном
пространстве.
Какой именно маршалинг будет реализован, зависит от реализации
COM объекта. Стандартный механизм реализован интерфейсом IDispatch.
16
Кроме того, объекты могут самостоятельно выполнять маршалинг, однако
это довольно сложно.
Отметим,
что
технология
Microsoft
Transaction
Server
(MTS)
обеспечивает дополнительную поддержку для удаленных объектов.
17
Последовательность создания простого COM объекта в Delphi
В данном разделе рассматривается алгоритм разработки простого COM
объекта, реализованного в виде библиотеки DLL, подключаемой к
приложению.
Процесс создания COM объекта состоит из следующих этапов:
1. Проектирование COM объекта.
2. Использование мастера Delphi для создания COM объекта.
3. Регистрация COM объекта.
4. Разработка клиентского приложения.
5. Тестирование COM объекта.
Проектирование COM объекта
На этом этапе необходимо выбрать вид используемого интерфейса.
Мастер предлагает использование в качестве базового (родительского)
любого из нескольких интерфейсов. В данном разделе будет предполагаться
выбор предлагаемого по умолчанию интерфейса IUnknown.
Далее необходимо определиться с типом сервера. Для всех типов
серверов, кроме удаленных, COM выполняет маршалинг автоматически и без
использования библиотеки типов, но для удаленных серверов надо
использовать библиотеку типов или самостоятельно выполнять маршалинг.
Использование библиотеки типов существенно облегчает разработку СОМ
объекта, предоставляя "паскалевский" доступ к методам СОМ объекта
Этап 1. Выполнить тему меню File\New\ActiveX\ActiveX Library, с
помощью которой будет создана заготовка проекта, включающего сервер. На
этом этапе потребуется только сохранить проект под заданным именем, в
данном примере SimpleCOM. В результате будет создан файл проекта
(единственный, без модулей) с таким содержимым.
library SimpleCOM;
uses ComServ;
exports
18
DllGetClassObject,
DllCanUnloadNow,
DllRegisterServer,
DllUnregisterServer;
{$R *.RES}
begin
end.
Этап 2. Выполнить тему меню File\New\ActiveX\COM Object для
добавления в проект объекта COM. В результате появится диалоговое окно, в
котором необходимо задать параметры нового объекта.
Теперь необходимо заполнить поля ввода предлагаемой мастером формы.
Окно
COM Object
Назначение
Wizard
ClassName
Имя разрабатываемого объекта COM.
Способ создания экземпляра объекта (instancing mode).
Instancing
Возможные значения: internal, single instance и multiplay
instance. Если COM объект используется только внутри
19
процесса, значение этого поля игнорируется.
Модель поточной обработки, указывающая на то, как
Threading model
клиентское приложение может обращаться к интерфейсу
COM объекта. Эта модель также влияет на то, как объект
будет регистрироваться, и на детали его реализации.
Implemented
Имя интерфейса – это имя класса с предшествующим
interfaces
символом I. Генерируется мастером автоматически.
Строка
Description
описания
класса,
используемая
при
его
регистрации.
Установка этого режима приводит к включению в проект
Include
Type библиотеки типов – генерации нового интерфейса для
Library
класса CoClass. При отключении этого режима этот
интерфейс надо разрабатывать самостоятельно.
Подключение
Mark
маршалинга,
главным
образом
для
interface внутрипроцессных серверов. Этот режим может быть
OLEAutomation
включен только при включенном режиме подключения
библиотеки типов.
При создании простого СОМ объекта можно оставить все установки,
предлагаемые мастером, без изменения, за исключением имени объекта. При
выборе имени объекта необходимо учесть, что Delphi автоматически добавит
в имя первый символ "Т".
После задания требуемых значений и нажатия кнопки OK появится
следующее диалоговое окно редактора библиотеки типов:
20
Еще до каких-либо установок в этом окне в проект будет добавлен
новый модуль, который мы сохраним под именем Main. Исходный код этого
модуля следующий:
unit Main;
interface
uses
Windows, ActiveX, Classes, ComObj, SimpleCOM_TLB, StdVcl;
type
TSmpCOM = class(TTypedComObject, ISmpCOM)
protected
{Declare ISmpCOM methods here}
end;
implementation
uses ComServ;
initialization
21
TTypedComObjectFactory.Create(ComServer, TSmpCOM, Class_SmpCOM,
ciInternal, tmApartment);
end.
Кроме того, будет сгенерирован и добавлен в проект файл с именем
SimpleCOM_TLB.pas, который обеспечивает наиболее удобный и простой
доступ к интерфейсам и методам СОМ объекта. Наибольший интерес
представляют следующие фрагменты кода этого файла:
const
{Глобальные идентификаторы библиотеки типов (LIBID_SimpleCOM),
сокласса (CLASS_SmpCOM), интерфейса СОМ объекта (IID_ISmpCOM)}
LIBID_SimpleCOM: TGUID = '{BB10BDEA-D908-11D5-A8B6-00C026266A1F}';
IID_ISmpCOM: TGUID = '{BB10BDEB-D908-11D5-A8B6-00C026266A1F}';
CLASS_SmpCOM: TGUID = '{BB10BDED-D908-11D5-A8B6-00C026266A1F}';
type
ISmpCOM = interface;
SmpCOM = ISmpCOM;
ISmpCOM = interface(IUnknown)
['{BB10BDEB-D908-11D5-A8B6-00C026266A1F}']
end;
{объявление сокласса, предоставляющего методы создания
экземпляра СОМ объекта}
CoSmpCOM = class
class function Create: ISmpCOM;
class function CreateRemote(const MachineName: string): ISmpCOM;
end;
Теперь вернемся к диалоговому окну редактора библиотеки типов.
Повторный вызов редактора библиотеки типов можно выполнить с помощью
команды F12 (Toggle Form/Unit) при активном окне с текстом файла
22
SimpleCOM_TLB.pas. С помощью этого редактора удобнее всего снабдить
наш СОМ объект дополнительными (кроме уже имеющегося ISmpCOM)
интерфейсами
и
собственно
методами,
которые
будут
реализовать
функциональность нашего объекта.
Этап 3. Для добавления новых (функциональных) методов к
имеющемуся интерфейсу ISmpCOM требуется выполнить такие действия:
В иерархическом списке в левом окне редактора выбрать интерфейс
ISmpCOM и щелкнуть на кнопке New Method в панели инструментов.
Заменить стандартное имя метода Method1 на требуемое, например,
Tangent. На странице Parameters необходимо задать тип возвращаемого
методом значения, а также имена и типы параметров. На остальных
страницах можно все оставить без изменений.
Проделываем ту же последовательность действий для другого метода –
Cube.
Щелкаем на кнопке Refresh Implementation для модификации исходных
текстов файлов Main.pas и SimpleCOM_TLB.pas.
23
Файл Main.pas теп ерь стал таким:
unit Main;
interface
uses Windows, ActiveX, Classes, ComObj, SimpleCOM_TLB, StdVcl;
type
TSmpCOM = class(TTypedComObject, ISmpCOM)
protected
{ Declare ISmpCOM methods here }
function Cube(Arg: Double): Double; stdcall;
function Tangent(Angle: Double): Double; stdcall;
end;
implementation
uses ComServ;
function TSmpCOM.Cube(Arg: Double): Double;
begin
{реализация функционального метода}
end;
function TSmpCOM.Tangent(Angle: Double): Double;
begin
{реализация функционального метода}
end;
initialization
{создание объекта "фабрика класса"}
TTypedComObjectFactory.Create(ComServer, TSmpCOM, Class_SmpCOM,
ciInternal, tmApartment);
end.
Если посмотреть на содержимое файла SimpleCOM_TLB.pas, то в нем
можно заметить такие изменения:
24
{В описание класса СОМ объекта добавлены функциональные методы
Tangent и Cube}
ISmpCOM = interface(IUnknown)
['{BB10BDEB-D908-11D5-A8B6-00C026266A1F}']
function Tangent(Angle: Double): Double; stdcall;
function Cube(Arg: Double): Double; stdcall;
end;
Эти
же
методы
добавлены
и
в
описание
класса
TSmpCOM = class(TOleServer).
Этап 4. Добавляем необходимый код в методы Tangent и Cube.
Этап 5. Подобным образом добавляем еще один интерфейс ISmpCOM2
и метод Summa, затем модифицируем исходный код метода Summa. На
странице Attributes в окне Parent interface необходимо выбрать IUnknown.
Этап 6. Вновь разработанный интерфейс требуется "привязать" к
объекту SmpCOM. Для этого необходимо в левом окне редактора выбрать
объект SmpCOM, а в правой части окна редактора – страницу Implements.
25
Щелкаем на списке имеющихся интерфейсов правой кнопкой мыши и
выбираем интерфейс ISmpCOM2.
Этап 7. Вновь щелкнем на кнопке Refresh Implementation для
модификации исходных текстов файлов Main.pas и SimpleCOM_TLB.pas.
Редактируем текст функции Summa. Содержимое файла Main.pas теперь
станет таким:
unit Main;
interface
uses
Windows, ActiveX, Classes, ComObj, SimpleCOM_TLB, StdVcl;
type
TSmpCOM = class(TTypedComObject, ISmpCOM, ISmpCOM2)
protected
function Cube(Arg: Double): Double; stdcall;
function Tangent(Angle: Double): Double; stdcall;
26
procedure Summa(Val1, Val2: Double; out Sum: OleVariant); safecall;
{Declare ISmpCOM methods here}
end;
implementation
uses ComServ;
function TSmpCOM.Cube(Arg: Double): Double;
begin
Result:=Arg*Arg*Arg;
end;
function TSmpCOM.Tangent(Angle: Double): Double;
begin
Result:=Sin(Angle)/Cos(Angle);
end;
procedure TSmpCOM.Summa(Val1, Val2: Double; out Sum: OleVariant);
begin
Sum:=Val1+Val2;
end;
initialization
TTypedComObjectFactory.Create(ComServer, TSmpCOM, Class_SmpCOM,
ciInternal, tmApartment);
end.
Этап 8. Регистрация COM объекта. Последнее, что осталось сделать – это
откомпилировать проект и зарегистрировать его в качестве сервера с
помощью команды Run\Register ActiveX Server. При этом регистрируется
также и библиотека типов, содержимое которой можно просмотреть с
помощью OLE View. Альтернативный способ – использование команды
tregsvr (MS DOS) или выполнение программы regsvr32.exe Windows.
27
Тестирование COM объекта. Этот этап зависит от функциональности
объекта и состоит в проверке работоспособности методов объекта.
Использование OLE View для получения информации о сервере. После
регистрации СОМ сервера информацию о нем можно просмотреть с
помощью OLE View. В разделе Object classes/All Objects информация о
сервере будет записана под именем, заданным в окне редактора библиотеки
типов Help String для сокласса. Таким образом, строка Help String выступает
в качестве внешнего имени сокласса (СОМ сервера). В разделе Type Libraries
можно найти информацию о библиотеке типов под именем, которое было
указано в окне Help String редактора библиотеки типов для библиотеки
типов.
28
Разработка и использование локального сервера
Выше был рассмотрен алгоритм реализации внутрипроцессного
сервера (in-proc server), который представляет собой библиотеку DLL,
загружаемую в адресное пространство клиентского приложения. В этом
разделе рассматривается разработка локального сервера (local server),
который
реализуется
как
самостоятельное
приложение
–
exe-файл.
Локальный сервер может быть также реализован и как DLL-библиотека, но в
этом случае для него требуется использовать в качестве "носителя"
специальное приложение, называемое суррогатом (surrogate). Суррогаты
обеспечиваются технологией DCOM и СОМ+.
В данном разделе будет рассмотрен способ реализации сервера в
качестве "обычного" приложения, разрабатываемого с помощью Delphi.
Разработка сервера
Этап 1. Создать типовое приложение, сохранить файл проекта под
требуемым именем.
Этап 2. Добавить к проекту библиотеку типов с помощью меню
File\New\ActiveX\Type Library. Библиотека типов будет автоматически
включена в проект (в предложение uses файла проекта). Подключение
полученного *_TLB.pas файла к другим модулям проекта надо выполнить
вручную.
Этап 3. Добавить к проекту СОМ объект с помощью меню
File\New\ActiveX\COM Object. Delphi создаст новый модуль-заготовку для
СОМ объекта (сервера) и подключит его к проекту. Можно было бы
пропустить этап 2, так как при включении в приложение нового СОМ
объекта, использующего библиотеку типов, файл библиотеки (*_TLB.pas)
добавляется в проект автоматически.
Этап 4. Как и для случая внутрипроцессного сервера, далее требуется
добавить в СОМ объект необходимые интерфейсы и методы для них с
29
помощью редактора библиотеки типов и добавить необходимый код в
функциональные методы СОМ объекта.
Этап 5. Откомпилировать проект и запустить на выполнение. При
первом
же
запуске
сервер
и
библиотека
будут
автоматически
зарегистрированы в реестре Windows.
Сервер можно также зарегистрировать, запустив его на выполнение с
параметром /RegServer командной строки:
Имя_сервера /RegServer
Сервер при этом на выполнение не запускается. Разрегистрация
сервера выполняется так:
Имя_сервера /UnRegServer
Разработка клиентского приложения
Приложение-клиент строится так же, как и для случая использования
внутрипроцессного сервера. При запуске клиентского приложения на
выполнение и вызове метода Create сокласса автоматически загружается
приложение-носитель запрашиваемого СОМ объекта и выполняются методы,
запрашиваемые клиентским приложением.
Сервер будет оставаться в памяти до тех пор, пока клиентское
приложение будет иметь ссылку на СОМ объект. Например, если интерфейс
(в клиентском приложении) описать как глобальную переменную, то сервер
будет оставаться в памяти до тех пор, пока эта переменная будет
существовать, т.е. до окончания работы клиентского приложения. Если
ссылка на интерфейс будет описана как локальная переменная в какой-либо
подпрограмме, то сервер будет выгружен из памяти при завершении работы
этой подпрограммы. При попытке закрытия приложения сервера в тот
момент, когда клиентское приложение имеет ссылку на СОМ объект, будет
выдано предупреждение (заголовок окна – COM Server Warning) о том, что
сервер используется другим приложением.
При запуске на выполнение нескольких экземпляров клиентских
приложений приложение-сервер будет запущено только в одном экземпляре,
30
если при включении СОМ объекта в проект были оставлены значения его
свойств, предлагаемые по умолчанию.
Запуск сервера сопровождается появлением главного окна приложения.
Это окно можно скрыть, если, например, в обработчике события OnPaint
вызвать метод Hide для того, чтобы скрыть главное окно приложения. В этом
случае при запуске приложения-сервера на выполнение (обычным образом
или из клиентского приложения) главное окно приложения появляться не
будет. Такое решение не очень подходит, так как при автономном запуске
локального сервера его главное окно должно появляться. Выход – анализ
командной строки на предмет наличия ключа /Embedding или –Embedding.
Например, можно добавить в приложение такой обработчик события OnPaint:
Procedure TForm1.FormPaint(Sender: TObject);
var
s : PChar;
Begin
s:=GetCommandLine();
if (Pos('/Embedding',s)<>0) or (Pos('-Embedding',s)<>0)
then Form1.Hide;
End;
По всей видимости, лучший выход – это перехват сообщения об
открытии или прорисовке окна или перекрытие соответствующего метода
главной формы приложения.
31
Интерфейсы
Виды интерфейсов:
 базовый интерфейс IUnknown;
 диспетчерский интерфейс IDispatch;
 диспинтерфейс (dispinterface);
 двойственный интерфейс (dual interface).
Интерфейс IUnknown поддерживает три метода и таблица виртуальных
методов VTBL для объекта, поддерживающего этот интерфейс, должна
иметь такой вид:
IUnknown.QueryInterface
IUnknown.AddRef
IUnknown.Release
Когда разработчик объекта добавляет свой интерфейс IMyInterface,
предоставляющий методы и свойства некоторого объекта, таблица VTBL
приобретает такой вид:
IUnknown.QueryInterface
IUnknown.AddRef
IUnknown.Release
IMyInterface.Method1
IMyInterface.Method2
другие методы и свойства IMyInterface
Для объекта, поддерживающего интерфейсы IUnknown и IDispatch, та
же таблица приобретает вид:
IUnknown.QueryInterface
IUnknown.AddRef
IUnknown.Release
IDispatch.GetIDsOfNames
32
IDispatch.GetTypeInfo
IDispatch.GetTypeInfoCount
IDispatch.Invoke
Отметим, что объекты, имеющие диспетчерский интерфейс, не могут
не иметь интерфейса IUnknown.
Интерфейс
IDispatch,
в
отличие
от
IUnknown,
предоставляет
возможность позднего связывания, что необходимо при программировании
на интерпретируемых языках. IDispatch не имеет индивидуальных методов
для доступа к методам и свойствам объекта автоматизации. Вместо этого он
предоставляет единственный метод Invoke, предназначенный для доступа к
свойствам COM объекта и вызова его методов.
Для вызова конкретного метода объекта необходимо, сначала,
получить номер метода с помощью функции GetIDsOfNames, а затем
полученный номер метода указать в вызове функции Invoke. Функция
GetIDsOfNames имеет такой заголовок:
Function GetIDsOfNames(const IID: TGUID; Names: Pointer; NameCount,
LocaleID: Integer; DispIDs: Pointer): HResult; stdcall;
Вызывающая программа должна передать через указатель Names имя
(или имена) требуемого метода, а номер (или номера) метода получает через
указатель DispIDs. Возвращаемое функцией значение описано в табл.
Возвращаемое значение
Описание
S_OK
Ошибки нет
E_OUTOFMEMORY
Недостаточно памяти
Одно
или
Возвращаемый
DISP_E_UNKNOWNNAME DISPIDs
более
имен
массив
содержит
неизвестны.
идентификаторов
значение
DISPID_UNKNOWN для каждого неизвестного
имени
33
DISP_E_UNKNOWNLCID
Не распознан идентификатор локализации LCID
Главный метод интерфейса IDispatch имеет описан как:
Function Invoke(DispID: Integer; const IID: TGUID; LocaleID: Integer; Flags:
Word;
var Params; VarResult, ExcepInfo, ArgErr: Pointer): HResult; stdcall;
Номер метода передается через параметр DispID, а фактические
параметры вызываемого метода – через Params, который представляет собой
указатель на структуру. Не вдаваясь в подробное толкование параметров
Invoke отметим только, что формирование списка фактических параметров
является сравнительно трудоемким занятием. Этих трудностей можно
избежать, так как вызывающая программа на Object Pascal может и не
использовать метод Invoke, что будет рассмотрено позже.
Метод GetTypeInfoCount
Function GetTypeInfoCount(out Count: Integer): HResult; stdcall;
позволяет узнать, доступна ли информация о типах данного COM объекта.
Если он возвращает значение Count=1, то информация доступна, а если
Count=0 – то нет.
Получить информацию о типах можно с помощью метода
Function GetTypeInfo(Index, LocaleID: Integer; out TypeInfo): HResult;
stdcall;
который возвращает информацию через указатель на интерфейс библиотеки
типов TypeInfo.
Диспинтерфейс (dispinterface) представляет собой объявление методов,
доступных
через
использование
интерфейс
методов
IDispatch.
интерфейса
Он
IDispatch
позволяет
благодаря
упростить
тому,
что
компилятор в этом случае имеет возможность выполнить контроль типов при
вызове методов объекта или обращении к его свойствам. При этом, тем не
менее, используется позднее связывание. В конечном итоге вызов метода
диспинтерфейса
Delphi
преобразует
в
вызов
метода
Invoke
с
34
соответствующими параметрами. Из этого следует, что на самом деле, в
отличие от раннего связывания, наличие вызываемого метода или свойства у
объекта не проверяется и не может быть проверено на этапе компиляции. Вот
пример описания диспинтерфейса:
ISomeDispInterf = dispinterface
['{40BC066A-D946-11D5-BE01-ABCDCE4D71F9}']
function Summa(X: Integer; Y: Integer): Integer; dispid 1;
end;
Если к объекту, имеющему интерфейс IDispatch, добавить свой
собственный интерфейс (custom interface), то мы получим двойственный
(дуальный) интерфейс (dual interface) и таблица VTBL в этом случае
приобретает такой вид:
IUnknown.QueryInterface
IUnknown.AddRef
IUnknown.Release
IDispatch.GetIDsOfNames
IDispatch.GetTypeInfo
IDispatch.GetTypeInfoCount
IDispatch.Invoke
IMyInterface.Method1
IMyInterface.Method2
другие методы и свойства IMyInterface
Дуальный интерфейс удобен тем, что предоставляет возможность
осуществления как раннего, так и позднего связывания методов COM
объекта. Для его реализации объявления методов собственного интерфейса
(производного от IUnknown) должны быть идентичны объявлениям методов
в диспинтерфейсе.
35
Разработка клиентских приложений для MS Office
Есть три основных способа создания клиентских приложений MS
Office:
1. Генерация интерфейсного модуля для сервера путем импорта
библиотеки типов
2. Включение в проект требуемого сервера как компонента Delphi,
например, WordApplication
3. Использование требуемого приложения MS Office как сервера
автоматизации.
В
этом
случае
клиентское
приложение
называют
контроллером автоматизации.
В чем разница между этими способами? В первых двух случаях при
разработке клиентского приложения используется раннее связывание, что
обеспечивает разработчику более комфортные условия, так как корректность
вызовов методов сервера проверяется еще на этапе компиляции и,
соответственно, облегчается отладка приложения. Достоинством третьего
способа является его универсальность и языковая независимость, так как
контролер автоматизации может быть реализован практически на любом из
ныне распространенных языков программирования.
Рассмотрим эти три способа на простом примере создания нового
файла с помощью MS Word и записи в него абзаца, состоящего из одной
строки.
Использование MS Word путем импорта библиотеки типов
Создадим обычное приложение, далее с помощью команды Project
| Import Type Library вызовем диалог по импорту библиотеки типов. В
появившемся диалоговом окне в списке библиотек типов надо выбрать
Microsoft Word Object Library и с помощью кнопки Create Unit создать файл с
библиотекой типов, приняв предлагаемое по умолчанию имя файла
Word_TLB.pas (проект WordAppTlb).
36
С учетом добавления кнопки текст модуля проекта может выглядить
таким образом:
unit Main;
{Использование сервера Word путем импорта библиотеки типов}
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, Word_Tlb, StdCtrls;
type
TMainForm = class(TForm)
RunButton: TButton;
procedure RunButtonClick(Sender: TObject);
end;
var
MainForm: TMainForm;
implementation
{$R *.dfm}
Procedure TMainForm.RunButtonClick(Sender: TObject);
var
FName,TmpName,NewTmp,DocType,Visible,VFalse,VTrue,
VNull,VSNull : OleVariant;
Err : integer;
App : _Application; {интерфейс}
Begin
App:=CoWordApplication.Create; {запуск сервера MSWord и получение
первого
интерфейса}
////////// готовим требуемые параметры
37
TmpName:='C:\WINDOWS\Application
Data\Microsoft\Шаблоны\Normal.dot';
NewTmp:=false; DocType:=0; Visible:=true;
////////// создаем новый документ
App.Documents.Add(TmpName,NewTmp,DocType,Visible);
////////// изменяем каталог для последующего сохранения файла
App.ChangeFileOpenDirectory('C:\Мои
документы\');{параметр
этого
метода
типа string, в связи с чем не можно обойтись без типа OleVariant}
////////// изменяем каталог для последующего сохранения файла
FName:='MyDoc.doc'; VFalse:=false; VNull:=0; VSNull:=''; VTrue:=true;
App.ActiveDocument.SaveAs(FName,VNull,VFalse,VSNull,VTrue,VSNull,
VFalse,VFalse,VFalse,VFalse,VFalse);
App.Selection.TypeText(OleVariant('Any text from Project with TLB'));
App.Selection.TypeParagraph;
App.ActiveDocument.Close(VTrue,VNull,VNull);
App.Quit(VTrue,VNull,VNull); {если этого не сделать, то Word не
завершится}
End;
END.
Как видно из текста (единственной) процедуры, основное неудобство
состоит в необходимости описания достаточно большого числа переменных
типа OleVariant, так как параметры многих методов сервера Word описаны
как параметры переменные. Неудобно также то, что справочной информации
по методам в Delphi Help нет, а доступны лишь оперативные подсказки по
именам и типам параметров.
38
Использование MS Word путем включения в проект компонента
Delphi
Замечание. Как и в случае импорта библиотеки типов, рассмотренном
в предыдущем разделе, для MS Office XP описываемый подход не работает.
Приведенный ниже пример относится к использованию Delphi 6 и MS Office
2000.
Для реализации этого способа надо создать обычное приложение и
добавить в него компонент TWordApplication (страница Servers палитры
компонент).
С учетом добавления кнопки текст модуля может выглядить таким
образом:
unit Main;
{Использование
сервера
MS
Word
как
компонента
Delphi
TWordApplication}
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, OleServer, Word2000;
type
TMainForm = class(TForm)
App: TWordApplication;
CreateDocButton: TButton;
procedure CreateDocButtonClick(Sender: TObject);
end;
var MainForm: TMainForm;
implementation
{$R *.dfm}
39
Procedure TMainForm.CreateDocButtonClick(Sender: TObject);
var
FName,TmpName,NewTmp,DocType,Visible,VFalse,VTrue,
VNull,VSNull : OleVariant;
Begin
TmpName:='C:\WINDOWS\Application
Data\Microsoft\Шаблоны\Normal.dot';
NewTmp:=false; DocType:=0; Visible:=true;
App.Documents.Add(TmpName,NewTmp,DocType,Visible);
App.ChangeFileOpenDirectory('C:\Мои документы\');
FName:='MyDoc.doc'; VFalse:=false; VNull:=0; VSNull:=''; VTrue:=true;
App.ActiveDocument.SaveAs(FName,VNull,VFalse,VSNull,VTrue,VSNull,
VFalse,VFalse,VFalse,VFalse,VFalse);
App.Selection.TypeText(OleVariant('Any text'));
App.Selection.TypeParagraph;
App.ActiveDocument.Close(VTrue,VNull,VNull);
End;
procedure
TMainForm.FormClose(Sender:
TObject;
var
Action:
TCloseAction);
begin
App.Quit;
end;
END.
Как видно из текста программы, использование сервера Word
посредством включения в проект компонента очень похоже на предыдущий
способ. В модуле Word2000.pas тип TWordApplication описан как класс,
производный от TOleServer:
TWordApplication = class(TOleServer)
40
Таким образом, от программиста просто скрывается за оболочкой
TWordApplication явное получение интерфейса.
Запуск сервера Word производится автоматически в момент запуска
приложения, если, конечно, Word в это время не запущен. Закрывать Word
надо самостоятельно, например, в методе FormClose. Однако, если Word был
запущен не приложением, то метод App.Quit все равно будет пытаться
завершить работу Word.
К достоинствам этого способа можно отнести тот факт, что в модуле
Word2000.pas определены константы, по именам которых можно догадаться
о назначении отдельных параметров методов и значениях свойств.
Разработка клиентских приложений для MS Office как контролеров
автоматизации
Клиентское приложение для любой из программ MS Office как
контролер автоматизации создается достаточно просто. В качестве примера
рассмотрим приложение, которое просто запускает MS Word и прекращает
работу с ним. Для разработки такого приложения надо:
1. Создать обычный проект, добавить на форму две кнопки и подключить
к главному модулю проекта модули ComObj и ActiveX.
2. В требуемом месте запустить копию сервера MS Word или
подключиться к уже имеющейся, если таковая есть.
3. Выполнить необходимые действия с использованием объектов MS
Word.
4. Закрыть приложение MS Word.
Вот
пример
текста
главного
модуля
проекта
Main.pas
(проект
WordAppAuto):
unit Main;
{Иллюстрация запуска и остановки приложения MS Word как сервера
автоматизации.
41
В качестве базового варианта взят пример из книги "Delphi 6 и технология
COM"
Любопытный факт: если запустить Word с помощью этого приложения, а
затем
"вручную" воспользоваться Word для открытия и редактирования какогонибудь
документа, то программа будет закрывать Word, но предварительно
появится окно
с предложением сохранить изменения в документе.
Код блокирования кнопок запуска и остановки Word добавлен для того,
чтобы
избежать ошибок.
Для компиляции проекта надо подключить модули ComObj и ActiveX.
}
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, ComObj, ActiveX, StdCtrls;
type
TMainForm = class(TForm)
StartWordButton: TButton;
StopWordButton: TButton;
procedure StartWordButtonClick(Sender: TObject);
procedure StopWordButtonClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
public
ServerIsRunning : boolean;
AppProgID : string;
42
App : variant;
end;
var
MainForm: TMainForm;
implementation
{$R *.dfm}
Procedure TMainForm.StartWordButtonClick(Sender: TObject);
var
unk : IUnknown;
Res : HResult;
Begin
AppProgID:='Word.Application';
ServerIsRunning:=false;
{пытаемся получить ссылку на запущенный экземпляр сервера.
Возможные возвращаемые GetActiveObject значения см. в Windows.pas}
Res:=GetActiveObject(ProgIDToClassID(AppProgID),nil,unk);
if Res=MK_E_UNAVAILABLE then {запускаем собственный экземпляр
сервера}
App:=CreateOleObject(AppProgID)
else
begin {получаем ссылку на сервер}
App:=GetActiveOLEObject(AppProgID);
ServerIsRunning:=true;
end;
App.Visible:=true; {делаем Word видимым просто для контроля.
Если Word не делать видимым, то он запущен, естественно, будет, но в
строке
статуса не появится}
43
StartWordButton.Enabled:=false;
StopWordButton.Enabled:=true;
{открываем какой-нибудь файл}
App.Documents.Open('D:\Workdir\Lit_All.doc',
EmptyParam,EmptyParam,EmptyParam,
EmptyParam,EmptyParam,EmptyParam,
EmptyParam,EmptyParam,EmptyParam);
End;
Procedure TMainForm.StopWordButtonClick(Sender: TObject);
Begin
{закрываем текущий активный документ}
App.ActiveDocument.Close; {документ можно закрыть и с помощью метода
App.Documents.Item(1).Close, где 1 - порядковый номер открытого
документа}
{если сервер был запущен нами, то нам его и останавливать. Если этого
не сделать, то прекратить его работу можно будет только с помощью
менеджера задач }
if not ServerIsRunning then App.Quit;
App:=UnAssigned;
StartWordButton.Enabled:=true;
StopWordButton.Enabled:=false;
End;
Procedure TMainForm.FormCreate(Sender: TObject);
Begin
StopWordButton.Enabled:=false;
End;
END.
44
Как найти информацию по объектам, предоставляемым серверами MS
Office?
Во-первых, это документация MSDN, а именно раздел Office Developer
Documentation. Конкретные примеры программирования (на языке VBA –
Visual Basic for Application) проще всего искать в файлах, соответствующих
маске vba*.chm. Например, файл vbawd10.chm содержит справочную
информацию по объектам сервера MS Word, а vbaxl9.chm – по MS Excel.
Кстати, библиотеку типов серверов приложений MS Office можно
импортировать из файлов *.olb. Например, библиотека типов Microsoft Word
2000 находится в файле MSWord9.olb.
Во-вторых, можно использовать такой сервис приложений MS Office,
как запись макросов. Например, чтобы воспользоваться этим средством в MS
Word, надо проделать такие операции:
1. Запустить на выполнение MS Word и включить запись макроса –
Сервис/Макрос/Начать запись. В появившемся диалоговом окне
можно изменить имя макроса и сферу его применения
2. Выполнить
«вручную»
те
операции,
которые
необходимо
запрограммировать. При этом возможно с помощью кнопок панели
управления макроса приостановить или отменить запись
3. После завершения всех операций остановить запись и открыть текст
макроса, например, с помощью команды Сервис/Макрос/Редактор
Visual Basic. Сгенерированный текст макроса можно использовать как
шаблон для записи текста программы.
Вот пример макроса с именем МойМакрос, сгенерированного при
выполнении таких операций: создание нового документа, сохранение его под
именем MyDoc.doc, ввод строки текста "Any text" и закрытие документа.
Sub МойМакрос ()
'
' МойМакрос Макрос
' Макрос записан 04.11.03 Victor
45
'
Documents.Add Template:= _
"C:\WINDOWS\Application
Data\Microsoft\Шаблоны\Normal.dot",
NewTemplate:=False, DocumentType:=0
Application.Keyboard (1058)
Application.Keyboard (1033)
ChangeFileOpenDirectory "C:\Мои документы\"
ActiveDocument.SaveAs
FileName:="MyDoc.doc",
FileFormat:=wdFormatDocument,
LockComments:=False,
Password:="",
AddToRecentFiles:=True,
WritePassword
:="", ReadOnlyRecommended:=False, EmbedTrueTypeFonts:=False,
SaveNativePictureFormat:=False,
SaveFormsData:=False,
SaveAsAOCELetter:=
False
Selection.TypeText Text:="Any text"
Selection.TypeParagraph
ActiveDocument.Close
End Sub
Результат «трансляции» этого макроса на язык Object Pascal выглядит
так:
App.Documents.Add('C:\WINDOWS\Application
Data\Microsoft\Шаблоны\Normal.dot',
False,0);
App.ChangeFileOpenDirectory('C:\Мои документы\');
App.ActiveDocument.SaveAs('MyDoc.doc',0,false,'',True,'',
False,
False,
False,
False, False);
46
App.Selection.TypeText('Any text');
App.Selection.TypeParagraph;
App.ActiveDocument.Close;
47
Простые СОМ-клиенты и СОМ-серверы
Для иллюстрации описанных приемов работы с СОМ приведем два
примера на языке C++, в которых используется СОМ. Они достаточно
просты и используют язык C++ и функции СОМ API. Всю работу будем
проделывать самостоятельно, без использования таких структур, как MFC
или
ATL.
Хотя
примеры
и
тривиальны,
на
них
вполне
можно
продемонстрировать основные приемы, используемые в СОМ. Ниже мы
применим новую библиотеку активных шаблонов фирмы Microsoft, чтобы
заново реализовать данный пример сервера.
Прежде чем мы начнем разработку, необходимо внести существенное
изменение
в
интерфейс
компонента.
Первоначальное
определение
абстрактного класса выглядит следующим образом:
class IMath : public IUnknown
{
public:
virtual long Add(long Op1, long Op2) = 0;
virtual long Subtract(long Op1, long Op2) = 0;
virtual long Multiply(long Op1, long Op2) = 0;
virtual long Divide (long Op1, long Op2) = 0;
};
Такой способ объявления порождает одну проблему. Каждый метод
СОМ-интерфейса должен возвращать значение типа HRESULT. В нашем
случае возвращается только результат операции. Поэтому нам требуется
возвратить HRESULT, а результат операции передать через параметр. Это
выглядит следующим образом:
class IMath : public IUnknown
{
public:
virtual HRESULT Add(long Op1, long Op2, long *pResult) = 0;
virtual HRESULT Subtract(long Op1, long Op2, long *pResult) = 0;
virtual HRESULT Multiply(long Op1, long Op2, long *pResult) = 0;
virtual HRESULT Divide (long Op1, long Op2, long *pResult) = 0;
};
48
Такая запись несколько необычна, поскольку известно, что возврат
методом значения вычислений уменьшает сложность программы. Теперь же
приходится работать с указателем на возвращаемый результат. Возвращение
значения типа HRESULT каждым методом является общим правилом в СОМ.
Однако существуют технические приемы (например, ключевое слово языка
определения интерфейсов retval), позволяющее приложению
клиента
трактовать каждый метод таким образом, как если бы он на самом деле
возвращал результат, а не HRESULT. Ниже мы рассмотрим эту возможность
на примере клиента.
Макросы STDMETHOD и STDMETHODIMP
Файлы
заголовков
СОМ
предоставляют
несколько
макросов,
используемых при объявлении и реализации СОМ-интерфейсов. Microsoft
рекомендует применять именно эти макросы, поскольку благодаря им
исключается возможность несовместимости программных сред. В первую
очередь разберемся с макросами STDMETHOD и STDMETHOD_, а также
с STDMETHODIMP и STDMETHODIMP_.
Вот
новое
определение
интерфейса IMath с использованием макроса STDMETHOD.
class IMath : public IUnknown
{
public:
STDMETHOD( Add(long , long , long *))PURE;
STDMETHOD( Subtract(long , long , long *))PURE;
STDMETHOD( Multiply(long , long , long *))PURE;
STDMETHOD( Divide (long , long , long *))PURE;
};
Результат развертывания макроса STDMETHOD_ зависит от целевой
среды и языка программирования: С или C++. Для среды Win32 с
использованием C++ определение этого макроса выглядит следующим
образом:
// OBJBASE.H
#define STDMETHODCALLTYPE
__stdcall
#define STDMETHOD(method)
virtual HRESULT
STDMETHODCALLTYPE method
49
#define STDMETHOD_(type,method) virtual type
STDMETHODCALLTYPE method
#define PURE
=0
#define STDMETHODIMP
HRESULT
STDMETHODCALLTYPE
#define STDMETHODIMP_(type) type STDMETHODCALLTYPE
Модификатор __stdcall указывает на необходимость соблюдения
определенного
соглашения
фирмы
Microsoft
относительно
вызовов,
используемого в API-функциях Win32. Это соглашение требует, чтобы
вызываемая
программа
очищала
стек
после
своего
вызова.
Модификатор PURE — всего лишь способ обозначить функцию как чисто
виртуальную (pure virtual, т.е. = 0).
Большая часть методов СОМ-интерфейсов возвращает стандартный
тип
HRESULT.
В
этом
состоит
единственное
макросами STDMETHOD и STDMETHOD_,
определенный
как STDMETHOD,
всегда
различие
поскольку
возвращает
между
метод,
данные
типа
HRESULT, а определение макроса STDMETHOD_ позволяет пользователю
задавать тип возвращаемого значения. Вот как в определении интерфейса
IClassFactory используется макрос STDMETHOD:
STDMETHOD (LockServer(BOOL fLock)) PURE;
//Разворачивается таким образом
virtual HRESULT _stdcall LockServer(BOOL fLock)= 0;
Макрос STDMETHOD применен для объявления методов интерфейса,
как в абстрактных определениях, так и в определениях класса. Единственным
отличием
является
модификатор
PURE.
Приведем
программу
для
производного класса:
class Math: public IMath
{
...
public:
//IUnknown
STDMETHOD(QueryInterface( REFIID, void** ));
STDMETHOD_(ULONG, AddRef());
STDMETHOD_(ULONG,Release());
50
//IMath
STDMETHOD(Add( long, long, long*));
STDMETHOD(Substract( long, long, long*));
STDMETHOD(Multiply( long, long, long*));
STDMETHOD(Divide( long, long, long*));
};
И, наконец, при реализации класса
используется
макрос
STDMETHODIMP. Приведем пример релизации класса Math:
STDMETHODIMP Math::Add( long Op1, long Op2, long *pResult )
{
*pResult = Op1 + Op2;
return S_OK;
}
STDMETHODIMP Math::Subtract( long Op1, long Op2, long *pResult )
{
*pResult = Op1 - Op2;
return S_OK;
)
STDMETHODIMP Math:: Multiply( long Op1, long Op2,long *pResult )
{
*pResult = Op1 * Op2;
return S_OK;
}
STDMETHODIMP_ (long) Math::Divide( long Op1, long Op2, long
*pResult )
{
*pResult = Op1 / Op2;
return S_OK;
}
Проект сервера
Приложение сервера обеспечивает реализацию интерфейса IMath и,
таким образом, создает и помещает на хранение компонент Math. Его
определение встречалось на протяжении всей данной главы, поэтому здесь
мы не будем останавливаться на нем подробно. Для разработки примера
сервера воспользуемся средой разработки Visual C++. Затем с помощью
51
утилиты AppWizard создадим проект Dynamic Link Library с именем Server.
Таким образом, мы получим простой проект Visual C++, не имеющий
исходных файлов.
Теперь необходимо объявить интерфейс абстрактного компонента и
его CLSID и IID. Наберите следующий текст и сохраните его в файле с
именем IMATH.H.
//
// imath.h
//
// {A888F560-58E4-11d0-A68A-0000837E3100}
DEFINE_GUID( CLSID_Math,
0xa888f560, 0x58e4, 0x11d0, 0xa6, 0x8a, 0x0, 0x0, 0x83, 0x7e,
0x31, 0x0);
// {A888F561-58E4-11d0-A68A-0000837E3100}
DEFINE_GUID( IID_IMath,
0xa888f561, 0x58e4, 0x11d0, 0xa6, 0x8a, 0x0, 0x0, 0x83, 0x7e,
0x31, 0x0);
class IMath : public IUnknown
{
public:
STDMETHOD( Add( long, long, long* )) PURE;
STDMETHOD( Subtract( long, long, long* )) PURE;
STDMETHOD( Multiply( long, long, long* )) PURE;
STDMETHOD( Divide( long, long, long* )) PURE;
};
Чтобы предоставить программе клиента информацию об определениях
интерфейса и идентификаторов CLSID и IID, необходимо отделить их от
собственно текста программы. Только благодаря этому программа клиента
может получить доступ к функциям компонента. Фактически клиент не
нуждается в CLSID (поскольку доступ к компоненту осуществляется с
помощью его ProgID), поэтому ничто не помешает нам расположить его
именно здесь.
Строки, начинающиеся с макроса DEFINE_GUID, можно ввести таким
же образом, как это сделано здесь, либо создать ваши собственные с
52
помощью
утилиты
GUIDGEN.
Теперь
необходимо
объявить
класс
компонента и фабрику классов для него. Чтобы сделать это, создайте файл с
именем МАТН.Н и введите следующие определения:
//
// math.h
//
#include "imath.h"
extern long g_lObjs;
extern long g_lLocks;
class Math : public IMath
{
protected:
// Reference count
long
m_lRef;
public:
Math();
~Math();
public:
// IUnknown
STDMETHOD(QueryInterface( REFIID, void** ));
STDMETHOD_(ULONG, AddRef());
STDMETHOD_(ULONG, Release());
// IMath
STDMETHOD(Add( long, long, long* ));
STDMETHOD(Subtract( long, long, long* ));
STDMETHOD(Multiply( long, long, long* ));
STDMETHOD(Divide( long, long, long* ));
};
class MathClassFactory : public IClassFactory
{
protected:
long
m_lRef;
public:
MathClassFactory();
53
~MathClassFactory();
// IUnknown
STDMETHOD( QueryInterface(REFIID, void** ));
STDMETHOD_(ULONG, AddRef());
STDMETHOD_(ULONG, Release());
// IClassFactory
STDMETHOD( CreateInstance(LPUNKNOWN, REFIID, void**));
STDMETHOD( LockServer(BOOL));
};
Большая часть этих строк встречалась вам и раньше. Класс Math
является производным по отношению к классу интерфейсов IMath, который в
свою очередь является производным для IUnknown. Объявим методы
IUnknown
и
IMath.
Отслеживание
общего
количества
экземпляров
компонента в DLL-файле и количества вызовов IClassFactory::LockServer
возлагается на две глобальные переменные. Затем объявляем класс для
фабрики классов компонента Math. Теперь можно взглянуть, на текст
программы. Создайте файл МАТН.СРР и введите в него следующее:
//
// Math.cpp
//
#include <windows.h>
#include "math.h"
//
// Math class implementation
//
// Constructors
Math::Math()
{
m_lRef = 0;
// Увеличить значение внешнего счетчика объектов
InterlockedIncrement( &g_lObjs );
}
// The destructor
Math::~Math()
54
{
// Уменьшить значение внешнего счетчика объектов
InterlockedDecrement( &g_lObjs );
}
В конструкторе внутренний счетчик инициализируется значением
нуль, а значение счетчика экземпляров для DLL-файла увеличивается. Затем
деструктор его уменьшает. Теперь добавьте в программу следующее:
STDMETHODIMP Math::QueryInterface( REFIID riid, void** ppv )
{
*ppv = 0;
if ( riid == IID_IUnknown || riid == IID_IMath )
*ppv = this;
if ( *ppv )
{
AddRef();
return( S_OK );
}
return (E_NOINTERFACE);
}
STDMETHODIMP_(ULONG) Math::AddRef()
{
return InterlockedIncrement( &m_lRef );
}
STDMETHODIMP_(ULONG) Math::Release()
{
if ( InterlockedDecrement( &m_lRef ) == 0 )
{
delete this;
return 0;
}
return m_lRef;
}
Таким образом, будет обеспечена реализация трех методов интерфейса
IUnknown.
Наш
компонент
поддерживает
только
два
интерфейса:
обязательный IUnknown и пользовательский IMath. Функция QueryInterface
проверяет возможность получения клиентом требуемого интерфейса, а также
55
возвращает указатель на указатель виртуальной таблицы компонента. Перед
возвратом
указателя
увеличивается
значение
внутреннего
счетчика
обращений. Это осуществляется с помощью вызова метода AddRef().
Реализации функций AddRef() и Release() ничем не отличаются от
использованных ранее.
STDMETHODIMP Math::Add( long lOp1, long lOp2, long* pResult )
{
*pResult = lOp1 + lOp2;
return S_OK;
}
STDMETHODIMP Math::Subtract( long lOp1, long lOp2, long* pResult )
{
*pResult = lOp1 - lOp2;
return S_OK;
}
STDMETHODIMP Math::Multiply( long lOp1, long lOp2, long* pResult )
{
*pResult = lOp1 * lOp2;
return S_OK;
}
STDMETHODIMP Math::Divide( long lOp1, long lOp2, long* pResult )
{
*pResult = lOp1 / lOp2;
return S_OK;
}
Мы получили текст относительно простой программы, но зато она
поможет нам быстро прогрессировать в понимании СОМ. Следующий текст
программы представляет собой реализацию класса C++ для фабрики классов.
MathClassFactory::MathClassFactory()
{
m_lRef = 0;
}
MathClassFactory::~MathClassFactory()
{
}
56
STDMETHODIMP MathClassFactory::QueryInterface( REFIID riid,
void** ppv )
{
*ppv = 0;
if ( riid == IID_IUnknown || riid == IID_IClassFactory )
*ppv = this;
if ( *ppv )
{
AddRef();
return S_OK;
}
return(E_NOINTERFACE);
}
STDMETHODIMP_(ULONG) MathClassFactory::AddRef()
{
return InterlockedIncrement( &m_lRef );
}
STDMETHODIMP_(ULONG) MathClassFactory::Release()
{
if ( InterlockedDecrement( &m_lRef ) == 0 )
{
delete this;
return 0;
}
return m_lRef;
}
STDMETHODIMP MathClassFactory::CreateInstance
( LPUNKNOWN pUnkOuter, REFIID riid, void** ppvObj )
{
Math* pMath;
HRESULT hr;
*ppvObj = 0;
pMath = new Math;
if ( pMath == 0 )
57
return( E_OUTOFMEMORY );
hr = pMath->QueryInterface( riid, ppvObj );
if ( FAILED( hr ) )
delete pMath;
return hr;
}
STDMETHODIMP MathClassFactory::LockServer( BOOL fLock )
{
if ( fLock )
InterlockedIncrement( &g_lLocks );
else
InterlockedDecrement( &g_lLocks );
return S_OK;
}
Большую часть из приведенного выше кода мы уже встречали.
Единственным
исключением
является
исходный
текст
процедуры
LockServer() . Хранилище сервера (DLL-файл) содержит переменную для
подсчета обращений, обеспечивающую, при необходимости, блокировку
сервера. Дальше будет показано, как именно используется этот счетчик.
После сохранения предыдущего файла МАТН.СРР создайте новый и
назовите его SERVER.CPP. Этот файл будет содержать главную программу
реализации хранилища нашего компонента. Файлы IMATH.H, МАТН.Н и
МАТН.СРР хранят тексты программ компонента. Теперь приведем код, с
помощью которого наш компонент помещается в хранилище.
//
// server.cpp : Defines the initialization routines for the DLL.
//
#include <windows.h>
#include <initguid.h>
#include "math.h"
long g_lObjs = 0;
58
long g_lLocks = 0;
STDAPI DllGetClassObject( REFCLSID rclsid, REFIID riid, void** ppv )
{
HRESULT
hr;
MathClassFactory *pCF;
pCF = 0;
// Make sure the CLSID is for our Expression component
if ( rclsid != CLSID_Math )
return( E_FAIL );
pCF = new MathClassFactory;
if ( pCF == 0 )
return( E_OUTOFMEMORY );
hr = pCF->QueryInterface( riid, ppv );
// Check for failure of QueryInterface
if ( FAILED( hr ) )
{
delete pCF;
pCF = 0;
}
return hr;
}
STDAPI DllCanUnloadNow(void)
{
if ( g_lObjs || g_lLocks )
return( S_FALSE );
else
return( S_OK );
}
Вначале мы включили в программу файл заголовков INITGUID.H,
чтобы определить GUID, используемый в DLL-файле. Затем определили две
глобальные переменные, отвечающие за подсчет обращений к хранилищу
компонента. Имейте в виду: для того чтобы DLL-файл стал настоящим
хранилищем компонентов, стандарт СОМ требует наличия В нем как
59
минимум двух функций (на самом деле их четыре, но остальные две будут
рассмотрены в следующих примерах). Сначала реализуем функцию
DllGetClassObject. СОМ вызывает эту точку входа по требованию клиента
компонента.
Указанная
функция
проверяет,
поддерживается
ли
затребованный клиентом компонент DLL-файлом. Удостоверившись в этом,
мы создаем экземпляр фабрики классов для объекта Math и вызываем
функцию QueryInterface из интерфейса, затребованного клиентом. Фабрика
классов объекта Math поддерживает только интерфейсы IUnknown и
IClassFactory. Если клиент или СОМ требует какой-либо другой интерфейс,
то возвращается код ошибки. Благодаря двум глобальным переменным
реализация функции DllCanUnloadNow намного упростилась. Проверяем,
имеются ли ожидающие обработки экземпляры компонента Math, и
подсчитываем количество вызовов функции LockServer. Если какая-либо из
проверок дает положительный результат, DLL-файл не может быть
выгружен.
Остался всего один шаг. Чтобы сделать обе функции, определенные в
файле SERVER.CPP, доступными для общего пользования, требуется создать
файл определений SERVER. DEF и ввести в него следующие строки:
;
; Server.def : Declares the module parameters for the DLL.
;
LIBRARY "SERVER"
DESCRIPTION 'SERVER Windows Dynamic Link Library'
EXPORTS
; Имена точек входа для внешнего пользования помещаются здесь
DllGetClassObject PRIVATE
DllCanUnloadNow PRIVATE
Прежде чем создавать проект, используйте элемент Files into project...
меню Insert для включения в проект файлов МАТН.СРР, SERVER.CPP и
SERVER. DEF, и лишь после этого приступайте к созданию проекта.
60
Последним шагом будет регистрация компонента Math. К этой статье
прилагается файл SERVER.REG, который выглядит следующим образом:
REGEDIT
HKEY_CLASSES_ROOT\Math.Component.1 = Chapter 6 Math
Component
HKEY_CLASSES_ROOT\Math.Component.1\CurVer =
Math.Component.1
HKEY_CLASSES_ROOT\Math.Component.1\CLSID = {A888F56058E4-11d0-A68A-0000837E3100}
HKEY_CLASSES_ROOT\CLSID\{A888F560-58E4-11d0-A68A0000837E3100} = Chapter 6 Math Component
HKEY_CLASSES_ROOT\CLSID\{A888F560-58E4-11d0-A68A0000837E3100}\ProgID = Math.Component.1
HKEY_CLASSES_ROOT\CLSID\{A888F560-58E4-11d0-A68A0000837E3100}\VersionIndependentProgID = Math.Component
HKEY_CLASSES_ROOT\CLSID\{A888F560-58E4-11d0-A68A0000837E3100}\InprocServer32 = c:\book\chap6\server\debug\server.dll
HKEY_CLASSES_ROOT\CLSID\{A888F560-58E4-11d0-A68A0000837E3100}\NotInsertable
Если вы использовали в примере имеющиеся GUID, то вам потребуется
изменить только информацию о расположении SERVER.DLL в файле
SERVER.REG в разделе InProcServer32. Однако если были сгенерированы
собственные GUID, необходимо во всех строках, содержащих CLSID,
обновить значения GUID. После того как вы введете необходимую
информацию или обновите файл SERVER.REG, включите, его в реестр с
помощью утилиты REGEDIT или дважды щелкните на названии этого файла
в окне Windows Explorer.
61
ПРИМЕЧАНИЕ
В Windows95 и Windows NT существуют программы редактирования
реестра REGEDIT.EXE и REGEDIT32.EXE. Зарегистрировать REG-файл
можно, дважды щелкнув на его пиктограмме в окне Windows Explorer.
Можно также ввести в командной строке start server.reg.
Теперь, после создания простого компонента Math, требуется
организовать доступ к нему из приложения клиента, чтобы проверить, как он
функционирует.
Приложение клиента
Клиентское приложение представляет собой простое консольное
приложение (Console Application) Win32. С помощью утилиты AppWizard
создайте консольное приложение с именем Client. Это опять-таки только
основа проекта, и AppWizard предоставит только МАК-файл.
После этого мы создадим файл CLIENT.CPP и включим в него
следующую программу:
//
// Client.cpp
//
#include <windows.h>
#include <tchar.h>
#include <iostream.h>
#include <initguid.h>
#include "..\server\imath.h"
int main( int argc, char *argv[] )
{
cout << "Initializing COM" << endl;
if ( FAILED( CoInitialize( NULL )))
{
cout << "Unable to initialize COM" << endl;
return -1;
62
}
char* szProgID = "Math.Component.1";
WCHAR szWideProgID[128];
CLSID clsid;
long lLen = MultiByteToWideChar( CP_ACP,
0,
szProgID,
strlen( szProgID ),
szWideProgID,
sizeof( szWideProgID ) );
szWideProgID[ lLen ] = '\0';
HRESULT hr = ::CLSIDFromProgID( szWideProgID, &clsid );
if ( FAILED( hr ))
{
cout.setf( ios::hex, ios::basefield );
cout << "Unable to get CLSID from ProgID. HR = " << hr << endl;
return -1;
}
IClassFactory* pCF;
// Получить фабрику классов для класса Math
hr = CoGetClassObject( clsid,
CLSCTX_INPROC,
NULL,
IID_IClassFactory,
(void**) &pCF );
if ( FAILED( hr ))
{
cout.setf( ios::hex, ios::basefield );
cout << "Failed to GetClassObject server instance. HR = " << hr <<
endl;
return -1;
}
// с помощью фабрики классов создать экземпляр
// компонента и получить интерфейс IUnknown.
IUnknown* pUnk;
hr = pCF->CreateInstance( NULL, IID_IUnknown, (void**) &pUnk );
// Release the class factory
pCF->Release();
63
if ( FAILED( hr ))
{
cout.setf( ios::hex, ios::basefield );
cout << "Failed to create server instance. HR = " << hr << endl;
return -1;
}
cout << "Instance created" << endl;
IMath* pMath = NULL;
hr = pUnk->QueryInterface( IID_IMath, (LPVOID*)&pMath );
pUnk->Release();
if ( FAILED( hr ))
{
cout << "QueryInterface() for IMath failed" << endl;
return -1;
}
long result;
pMath->Multiply( 100, 8, &result );
cout << "100 * 8 is " << result << endl;
pMath->Subtract( 1000, 333, &result );
cout << "1000 - 333 is " << result << endl;
cout << "Releasing instance" << endl;
pMath->Release();
cout << "Shuting down COM" << endl;
CoUninitialize();
return 0;
}
В начале программы мы поместили файл заголовков IMATH.H из
проекта сервера. Для определения GUID компонентов перед ним включен
файл INITGUID.H. Функция main в первую очередь обеспечивает
инициализацию библиотеки СОМ. В примере для определения CLSID
используется ProgID компонента. Однако, прежде чем мы сможем вызвать
CLSIDFromProgID,
необходимо
преобразовать
ProgID
(для
которого
используется кодировка ANSI) в строку с кодировкой Unicode. Все вызовы
СОМ, OLE и ActiveX имеют встроенные реализации Unicode. Поэтому до
64
передачи
строк
в
любую
API-функцию
СОМ
они
должны
быть
преобразованы в вызовы с кодировкой Unicode.
После
получения
CLSID
компонента
вызываем
функцию
CoGetClassObject и запрашиваем указатель на интерфейс фабрики классов
для компонента Math. После этого с помощью вызова CreateInstance создаем
экземпляр компонента Math. Затем освобождаем интерфейс фабрики классов.
Функция CreateInstance возвращает указатель на интерфейс IUnknown, с
помощью которого мы в конце концов запрашиваем IMath. Получив
указатель на него, используем сервисы компонента для выполнения
некоторых простых операций.
По окончании всего этого мы освобождаем указатель на интерфейс
IMath и вызываем CoUninitialize (это необходимо сделать до завершения
работы приложения).
После ввода описанной выше программы включите в проект клиента
файл CLIENT.CPP и постройте приложение. При пошаговой отладке клиента
можно дойти даже до текста программы сервера. Не пожалейте времени и
хорошо разберитесь в этих простых примерах клиента и сервера СОМ.
Данные примеры помогут вам понять, что представляет собой СОМразработка.
65
Технология объектно-ориентированного подхода CORBA
CORBA (Common
Object
Request
Architecture —
Broker
общая
архитектура брокера объектных запросов)– это набор спецификаций,
возникающий в результате самого широкого обсуждения накопившихся
реальных проблем, в котором участвуют и разработчики и потребители
технологий.
В
результате
предлагающий
решение
существующих
технологий
такого
обсуждения
рассматриваемых
и
технологий
создается
вопросов
документ,
на
ближайшей
уровне
перспективы.
Достоинством опережающей разработки спецификации по сравнению с
реализацией
является
возможность
для
независимых
разработчиков
создавать потенциально совместимые продукты, не ограничивая свободы
выбора языков, ОС, аппаратных платформ, и не диктуя выбора конкретного
технологического решения.
Альтернативные подходы, наиболее ярко выраженные в политике
Microsoft и Sun, не соответствуют современным тенденциям развития
технологий, согласно которым диктат одного производителя (хотя бы и с
самыми лучшими намерениями) в общем, создает больше проблем, чем
решает. Здесь имеются в виду не только технологии. Примером этого служит
ОС Windows, которая имеет больше пользователей, чем все остальные вместе
взятые,
и
при
этом
большинство
рассматривает
этот
выбор
как
вынужденный.
Любой частный стандарт, поддерживаемый отдельным производителем
(оставляя в стороне вопрос о его открытости) вынужден следовать
исторически сложившейся линии развития, что рано или поздно входит в
противоречие с интересами потребителя.
Проблема
монопольных
стандартов
состоит
в
заведомо
протекционистской политике владельца стандарта, проявляющейся на других
связанных рынках, в случае с Microsoft, это ОС. Усиленное продвижение
СОМ на платформе Windows делает перенос этого стандарта на другие
66
платформы экономически невыгодным, заставляя разработчика вступать в
заведомо проигрышную конкуренцию с Microsoft.
CORBA является концепцией, а не ее реализацией. Когда мы говорим
"COM", то понимаем под этим скорее набор конкретных средств – элементов
операционной системы, библиотек, утилит и т.п., являющихся составной
частью того, что называется Microsoft Windows. Под термином "CORBA"
понимается именно сложная и развитая концепция, сформулированная на
уровне специального языка описаний – IDL. Реализации же этой концепции
могут сильно отличаться друг от друга по различным критериям, наиболее
важным
в
том
или
другом
случае.
VisiBroker
(разработки
Visigenic/Borland/Inprise/Corel) и Application Server, BEA WebLogic, Iona
Orbix, Oracle Application Server и "картриджи" Oracle, IBM BOSS – все эти
продукты используют те или иные возможности CORBA.
Под "стандартом" применительно к CORBA понимается то, что
официально утверждено консорциумом OMG. Надо сказать, что это очень
высокий уровень "легитимности", так как авторитет OMG в компьютерном
мире чрезвычайно высок. OMG представляет собой некоммерческую
организацию, являющуюся содружеством разработчиков программного
обеспечения и его потребителей, объединивших свои усилия для создания
спецификаций этой технологии. В настоящий момент в OMG состоит более
800 членов, включая всех сколько-нибудь серьезных производителей
программного обеспечения (и даже c недавнего времени Microsoft). Первая
спецификация CORBA появилась в 1991 г. Новые возможности официально
считаются добавленными в CORBA в момент утверждения соответствующей
спецификации. Как правило, в разработке спецификации участвуют
крупнейшие специалисты в данной области. Разработка реализации – задача
конкретной фирмы. Обычно от утверждения спецификации до появления
высококачественной реализации проходит довольно много времени – иногда
несколько лет. В настоящий момент стандартизовано отображение языка IDL
на 6 языков программирования – Ada, C, C++, Cobol, Java и Smalltalk.
67
Существуют также отображения на Pascal (точнее, Delphi), Perl, Python и еще
несколько языков, но они не стандартизованы.
Объекты CORBA можно рассматривать как экземпляры (instances)
некоторого метатипа, причем и метатип, и сами объекты существуют вне
связи с конкретной программой на конкретном языке. Этот метатип в
CORBA называется «интерфейсом».
Интерфейс
К счастью, для новичка в мире CORBA понять, что же такое
интерфейс, не составляет никакого труда.
Интерфейс в CORBA – это логически сгруппированный набор методов
и атрибутов. Каждому интерфейсу присваивается имя, уникальное в пределах
одной распределенной системы. В отличие от СОМ в CORBA нет бинарного
стандарта интерфейсов. Вместо этого существует стандартный язык
описаний IDL. Так уж получилось, что языки с названием IDL существуют в
трех различных технологиях – OSF/DCE, Microsoft/COM и OMG/CORBA.
Эти языки во многом похожи, поскольку предназначены для одного и того
же, но OMG/IDL несколько отличается от своих «однофамильцев».
За его основу был взят язык C++ (его описательная часть и директивы
препроцессора), поэтому читатель, знакомый с C++, при работе с IDL будет
чувствовать себя вполне комфортно.
Вот пример объявления интерфейсов на языке IDL:
exception MyException {};
interface MyBaseInterface
{
long MyMethod_1(in long i, out string str);
void MyMethod_2 () raises (MyException);
};
interface MyDerivedInterface : MyBaseInterface {
octet MyMethod_3 (inout double d);};
68
В настоящий момент последним стандартом CORBA является стандарт
версии 2.3. В нем понятия «объект» и «интерфейс» связаны, так сказать,
отношением «один к одному» – один объект не может поддерживать
несколько интерфейсов. В стандарте CORBA 3.0, принятие которого
ожидается к концу 2000 г, должна появиться возможность создания объектов,
поддерживающих несколько интерфейсов.
С помощью приведенного выше примера определения интерфейса (и,
естественно, определенного программного кода) вы можете, предположим,
создать
5
объектов
типа
MyBaseInterface
и
10000
объектов
MyDerivedInterface. Каждый из этих объектов сопоставлен со своим типом и,
кроме этого, имеет свой уникальный идентификатор.
Еще раз повторим – создание вышеуказанных 10005 объектов в общем
случае никак не связано с «захватом» ни ресурсов компьютера (в первую
очередь памяти), ни сетевых ресурсов.
Сервант
Итак, вы можете создать CORBA-объект и даже установить с ним
связь. В общем случае этого совершенно недостаточно, чтобы использовать
его в конкретной программе. Функциональность CORBA-объекта недоступна
для клиента до тех пор, пока в программе (серверном приложении) не создан
объект, который позволяет получить доступ к методам, объявленным в IDLинтерфейсе. Этот объект (реализованный на C++, Java, C, Cobol, Ada,
Smalltalk или некоторых других языках) и называется «сервантом».
Конечно, в зависимости от используемого языка программирования,
серванты реализуются по-разному. Для объектно-ориентированных языков
сервант является экземпляром (instance) некоторого класса, методы которого
реализуют нужную функциональность. Такой класс часто называют «классом
реализации».
За время существования CORBA-объекта с ним может быть
сопоставлено множество различных реализаций сервантов (но не более
одного за раз). Более того, они могут содержаться в адресном пространстве
69
различных приложений. Эти приложения могут быть даже запущены на
различных компьютерах.
Часто говорят, что сервант является «инкарнацией» CORBA-объекта.
Связь между сервантами и CORBA-объектами является хотя и строго
формализованной, но очень гибкой. Сервант может быть создан раньше или
позже CORBA-объекта; один сервант может «обслуживать» как один, так и
несколько (иногда сотни тысяч и миллионы) CORBA-объектов. Явное
разделение циклов жизни CORBA-объектов и их сервантов (а именно
серванты потребляют реальные ресурсы) – один из столпов, на которых
базируется очень высокая масштабируемость CORBA-приложений.
Объектная ссылка
Единственная сложность, связанная с пониманием смысла термина
«объектная ссылка», состоит в том, что он используется в двух различных
смыслах.
Есть объектная ссылка «мира CORBA», которая представляет собой
закодированную информацию о CORBA-объекте. Она включает имя хоста,
порта TCP/IP (или координаты Репозитария Реализаций), конечно же,
уникальный идентификатор данного CORBA-объекта и множество другой
информации, позволяющей клиенту установить связь с серверным объектом
через
границы
языков
программирования,
операционных
систем
и
аппаратных платформ. Операции с объектной ссылкой невозможны для
клиента, за исключением того, что клиент может превратить ее в строку и
записать в файл или базу данных. Впоследствии кто угодно может считать
такую строку и преобразовать ее опять в объектную ссылку.
В другом понимании «объектная ссылка» – это переменная того или
иного языка программирования, с помощью которой клиент осуществляет
вызов удаленных методов. В последующих разделах будут приведены
примеры получения и использования такой объектной ссылки. В дальнейшем
все упоминания объектных ссылок относятся именно к этому, второму, типу
объектных ссылок.
70
Концептуально
переменная
типа
«объектная
ссылка»
является
указателем на так называемый «proxy-объект», который существует на
стороне клиента и обеспечивает выполнение удаленных вызовов. Сам proxyобъект сделан недоступным для программиста; связано это с тем, что его
создание – задача не клиентского приложения, а самого ORB’а. Логически с
каждым proxy-объектом сопоставлена отдельная объектная ссылка, и под
копированием объектной ссылки следует понимать создание как нового
proxy-объекта, так и настроенного на него нового «указателя». Разумеется, в
реальных
реализациях
физического
копирования
proxy-объекта
не
происходит – как всегда в таких случаях, используется механизм счетчика
ссылок.
Очень важно отчетливо понимать, что копирование (или уничтожение)
объектных ссылок на стороне клиента влияет исключительно на клиентское
приложение. Неправильное ведение счетчика ссылок в самом худшем случае
приведет к продолжению физического существования в клиентском
приложении ненужного proxy-объекта. Никакого отношения к серверному
объекту эти действия не могут иметь в принципе. И создание, и уничтожение
сервантов или серверных CORBA-объектов – задача серверного приложения.
Философия CORBA состоит в том, чтобы клиент посылал сообщения
«установить связь с существующим объектом» и «разорвать с ним связь», а
не «создать серверный объект» и «уничтожить его». Разумеется, клиент
может инициировать создание Corba-объектов вызвав у удаленного объекта
специально предусмотренный для этого программистом (автором объекта)
метод.
71
Download