DCOM

Основные понятия и определения

Технология COM как спецификация и реализация

Расширения COM (COM extensions)

Базовый интерфейс Iunknown

Указатели на интерфейсы COM объекта

COM серверы

Фабрика классов и класс CoClass

Последовательность событий при обращении клиента к серверу COM

Сервер в клиентском процессе (In-process server)

Локальный сервер (local server)

Удаленный сервер (remote server)

Маршалинг

Проектирование COM объекта

Разработка сервера

Разработка клиентского приложения

Использование MS Word путем импорта библиотеки типов

Использование MS Word путем включения в проект компонента Delphi

Разработка клиентских приложений для MS Office как контролеров автоматизации

Простые СОМ-клиенты и СОМ-серверы

Макросы STDMETHOD и STDMETHODIMP

Проект сервера

Приложение клиента

Технология объектно-ориентированного подхода CORBA

Интерфейс

Сервант

Объектная ссылка

Введение в 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, включая в новые технологии такие составляющие:

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

Технология COM как спецификация и реализация

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

COM как реализация представляет собой библиотеку (файлы OLE32.dll OLEAut32.dll), которая предоставляет ряд основных служб, поддерживающих описанные спецификации. Библиотека COM содержит набор стандартных интерфейсов, определяющих основную функциональность объектов COM, и небольшой набор функций API, разработанных для целей создания и управления объектами COM.

Расширения COM (COM extensions)

Так как COM является развивающейся технологией, она может быть расширена за рамки базисных служб. COM является основой для других технологий, таких как автоматизация (Automation – вместо прежнего термина OLE Automation), элементы ActiveX и активные документы (Active Documents).

Кроме того, в настоящее время возможно создание таких объектов COM, которые могут взаимодействовать с Microsoft Transaction Server (MTS). MTS – это система обработки транзакций, предназначенная для построения, развертывания и управления большими Intranet и Internet приложениями-серверами. Хотя MTS архитектурно и не является частью COM, она разработана для расширения возможностей COM в большой, распределенной среде.

Составляющие приложений COM

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

Элемент Назначение
COM Interface Средство, с помощью которого объект COM предоставляет свои функциональные возможности (службы) для внешних клиентов. Объект COM снабжает интерфейсом каждый набор методов и свойств. Любой объект COM имеет один или более интерфейсов
COM server Некоторый модуль (EXE, DLL или OCX), который содержит код для объекта COM
COM client Программный код, который получает требуемые услуги от сервера через интерфейс(ы) объекта COM. Клиент знает, что он хочет получить от сервера, но не знает как его запрос выполняется внутри сервера. В большинстве случаев клиент реализуется как Automation controller (то же, что и ActiveX-клиент)
Type Library Библиотека типов, которая содержит описание COM объекта, его интерфейсов и методов, а также их GUID идентификаторы. Информация из библиотеки записывается в системный реестр и используется клиентским приложением.
Class Factory Фабрика классов, экземпляр объекта которой создает COM объект. Сам объект фабрики классов создается COM сервером при запросе клиентским приложением первого интерфейса

Интерфейсы COM

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

Например, каждый COM объект имеет базовый интерфейс IUnknown, который сообщает, какие интерфейсы объекта доступны клиенту. Любой интерфейс сообщает клиенту, какие возможности он предоставляет.

Ключевыми аспектами интерфейсов объектов COM являются следующие:

Базовый интерфейс Iunknown

Этот интерфейс, который должны поддерживать все COM объекты, включает следующие подпрограммы:

Метод QueryInterface, объявленный как

Function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;

возвращает клиенту указатель на запрошенный интерфейс IID. С помощью полученного указателя клиент может вызвать любой из реализованных методов интерфейса. В качестве IID клиент может указать тип класса интерфейс – в этом случае компилятор самостоятельно извлекает соответствующий GUID.

Методы AddRef и Release используются для того, чтобы объект COM мог самостоятельно отслеживать продолжительность своего существования. Эти методы просто изменяют число ссылок на объект. Когда число ссылок на объект становится равным нулю, объект удаляется из памяти COM сервером.

Указатели на интерфейсы COM объекта

Указатель на интерфейс является 32-битным указателем, который ссылается на указатель на таблицу vtable (рис 2.). Эта таблица является массивом указателей, каждый из которых, в свою очередь, указывает на реализацию метода. Таблица vtable является общей для всех экземпляров объекта, но для каждого из экземпляров создается свой набор данных.

Рисунок 5

Рисунок 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 объекту, фабрика класса создает экземпляр объекта и регистрирует экземпляр объекта для этого конкретного клиента. Если в это время другой клиент обращается к объекту, фабрика классов для него также создает (другой) экземпляр объекта.

Любой класс CoClass должен иметь фабрику классов и идентификатор класса CLSID, так что экземпляр COM объекта этого класса может быть создан извне, т.е. из другого модуля. Благодаря наличию у классов CoClass уникального идентификатора CLSID, они могут быть обновлены в любое время, как только для класса разработан новый интерфейс. Новый интерфейс может использовать модифицированные или новые методы и это не оказывает никакого влияния на прежние версии. В случае использования обычных библиотек DLL подобная ситуация прежде была типичной проблемой.

Последовательность событий при обращении клиента к серверу 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"

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. Реализация библиотеки сервера в соответствии со спецификацией COM должна экспортировать, в общем случае, всего 4 функции: DllGetClassObject, DllCanUnloadNow, DllRegisterServer и DllUnregisterServer.

Способы реализации СОМ серверов

При работе с объектами COM клиент не знает, где находится объект. Он просто обращается к объекту посредством его интерфейса. Далее библиотека COM выполняет все необходимые шаги для удовлетворения вызова клиента.

Эти шаги зависят от конкретного местонахождения объекта COM: в том же процессе, что и клиент, в другом процессе на машине клиента или на другой машине в сети. В зависимости от этого различают три вида серверов COM.

Сервер в клиентском процессе (In-process server)

Это библиотека DLL, которая выполняется в адресном пространстве процесса клиента. Например, элемент ActiveX, внедренный в Web страницу, выполняется в Internet Explorer или другом браузере. Это значит, что объект ActiveX загружается на машину клиента и выполняется в том же процессе, что и Web браузер. Клиент обращается к объекту COM путем прямого вызова интерфейса COM.

Рисунок 11

Рисунок 3. Сервер в клиентском процессе

Локальный сервер (local server)

Он представляет собой другое приложение (файл *.exe), которое выполняется в другом адресном пространстве, но на том же компьютере, что и клиентское приложение. Например, обработка таблицы Excel, внедренной в документ Word, выполняется приложением Excel. Локальный сервер связывается с клиентом посредством COM библиотек.

Рисунок 12

Рисунок 4. Локальный сервер

Когда объект COM принадлежит другому процессу на том же компьютере, что и клиентское приложение, или вовсе на другом компьютере (в сети), COM использует так называемого заместителя (proxy) для того, чтобы инициировать удаленный вызов процедуры (remote procedure call – RPC). Так как заместитель находится в клиентском процессе, то с точки зрения клиента обращения к интерфейсам выглядят так же, как и для случая размещения сервера в клиентском процессе. Заместитель перехватывает вызовы клиента и отправляет их туда, где находится сервер. Механизм, который позволяет клиенту получить доступ к объекту, находящемуся в другом процессе или на другом компьютере (невидимо для клиента), называется маршалингом или маршализацией.

Удаленный сервер (remote server)

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

Рисунок 7

Рисунок 5. Удаленный сервер

Различие между локальным и удаленным сервером состоит в применяемом способе (и инструментальных средствах) взаимодействия клиента и сервера: в первом случае используется COM, а во втором – DCOM.

Если сервер реализован как библиотека (а библиотека не может выполняться как самостоятельное приложение), то COM создает специальное приложение-суррогат (surrogate), в рамках которого и выполняется библиотека.

Маршалинг

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

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

Какой именно маршалинг будет реализован, зависит от реализации COM объекта. Стандартный механизм реализован интерфейсом IDispatch. Кроме того, объекты могут самостоятельно выполнять маршалинг, однако это довольно сложно.

Отметим, что технология Microsoft Transaction Server (MTS) обеспечивает дополнительную поддержку для удаленных объектов.

Простые СОМ-клиенты и СОМ-серверы

Для иллюстрации описанных приемов работы с СОМ приведем два примера на языке 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;
};

Такая запись несколько необычна, поскольку известно, что возврат методом значения вычислений уменьшает сложность программы. Теперь же приходится работать с указателем на возвращаемый результат. Возвращение значения типа 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
#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());

//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++. Затем с помощью утилиты 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, можно ввести таким же образом, как это сделано здесь, либо создать ваши собственные с помощью утилиты 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();
~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()
{
// Уменьшить значение внешнего счетчика объектов
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 проверяет возможность получения клиентом требуемого интерфейса, а также возвращает указатель на указатель виртуальной таблицы компонента. Перед возвратом указателя увеличивается значение внутреннего счетчика обращений. Это осуществляется с помощью вызова метода 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() {
}

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 )
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;
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-файл стал настоящим хранилищем компонентов, стандарт СОМ требует наличия В нем как минимум двух функций (на самом деле их четыре, но остальные две будут рассмотрены в следующих примерах). Сначала реализуем функцию 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, и лишь после этого приступайте к созданию проекта. Последним шагом будет регистрация компонента 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
 = {A888F560-58E4-11d0-A68A-0000837E3100}

HKEY_CLASSES_ROOT\CLSID\{A888F560-58E4-11d0-A68A-0000837E3100}
 = Chapter 6 Math Component
HKEY_CLASSES_ROOT\CLSID\{A888F560-58E4-11d0-A68A-0000837E3100}
\ProgID = Math.Component.1
HKEY_CLASSES_ROOT\CLSID\{A888F560-58E4-11d0-A68A-0000837E3100}
\VersionIndependentProgID = Math.Component
HKEY_CLASSES_ROOT\CLSID\{A888F560-58E4-11d0-A68A-0000837E3100}
\InprocServer32 = c:\book\chap6\server\debug\server.dll
HKEY_CLASSES_ROOT\CLSID\{A888F560-58E4-11d0-A68A-0000837E3100}
\NotInsertable

Если вы использовали в примере имеющиеся GUID, то вам потребуется изменить только информацию о расположении SERVER.DLL в файле SERVER.REG в разделе InProcServer32. Однако если были сгенерированы собственные GUID, необходимо во всех строках, содержащих CLSID, обновить значения GUID. После того как вы введете необходимую информацию или обновите файл SERVER.REG, включите, его в реестр с помощью утилиты REGEDIT или дважды щелкните на названии этого файла в окне Windows Explorer.

ПРИМЕЧАНИЕ
В 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;
}

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();

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. Поэтому до передачи строк в любую API-функцию СОМ они должны быть преобразованы в вызовы с кодировкой Unicode.

После получения CLSID компонента вызываем функцию CoGetClassObject и запрашиваем указатель на интерфейс фабрики классов для компонента Math. После этого с помощью вызова CreateInstance создаем экземпляр компонента Math. Затем освобождаем интерфейс фабрики классов. Функция CreateInstance возвращает указатель на интерфейс IUnknown, с помощью которого мы в конце концов запрашиваем IMath. Получив указатель на него, используем сервисы компонента для выполнения некоторых простых операций.

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

После ввода описанной выше программы включите в проект клиента файл CLIENT.CPP и постройте приложение. При пошаговой отладке клиента можно дойти даже до текста программы сервера. Не пожалейте времени и хорошо разберитесь в этих простых примерах клиента и сервера СОМ. Данные примеры помогут вам понять, что представляет собой СОМ-разработка.

Технология объектно-ориентированного подхода CORBA

CORBA (Common Object Request Broker Architecture — общая архитектура брокера объектных запросов)– это набор спецификаций, возникающий в результате самого широкого обсуждения накопившихся реальных проблем, в котором участвуют и разработчики и потребители технологий. В результате такого обсуждения создается документ, предлагающий решение рассматриваемых вопросов на уровне существующих технологий и технологий ближайшей перспективы. Достоинством опережающей разработки спецификации по сравнению с реализацией является возможность для независимых разработчиков создавать потенциально совместимые продукты, не ограничивая свободы выбора языков, ОС, аппаратных платформ, и не диктуя выбора конкретного технологического решения.

Альтернативные подходы, наиболее ярко выраженные в политике Microsoft и Sun, не соответствуют современным тенденциям развития технологий, согласно которым диктат одного производителя (хотя бы и с самыми лучшими намерениями) в общем, создает больше проблем, чем решает. Здесь имеются в виду не только технологии. Примером этого служит ОС Windows, которая имеет больше пользователей, чем все остальные вместе взятые, и при этом большинство рассматривает этот выбор как вынужденный.

Любой частный стандарт, поддерживаемый отдельным производителем (оставляя в стороне вопрос о его открытости) вынужден следовать исторически сложившейся линии развития, что рано или поздно входит в противоречие с интересами потребителя.

Проблема монопольных стандартов состоит в заведомо протекционистской политике владельца стандарта, проявляющейся на других связанных рынках, в случае с Microsoft, это ОС. Усиленное продвижение СОМ на платформе Windows делает перенос этого стандарта на другие платформы экономически невыгодным, заставляя разработчика вступать в заведомо проигрышную конкуренцию с 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. Существуют также отображения на 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);};

В настоящий момент последним стандартом CORBA является стандарт версии 2.3. В нем понятия «объект» и «интерфейс» связаны, так сказать, отношением «один к одному» – один объект не может поддерживать несколько интерфейсов. В стандарте CORBA 3.0, принятие которого ожидается к концу 2000 г, должна появиться возможность создания объектов, поддерживающих несколько интерфейсов.

С помощью приведенного выше примера определения интерфейса (и, естественно, определенного программного кода) вы можете, предположим, создать 5 объектов типа MyBaseInterface и 10000 объектов MyDerivedInterface. Каждый из этих объектов сопоставлен со своим типом и, кроме этого, имеет свой уникальный идентификатор.

Еще раз повторим – создание вышеуказанных 10005 объектов в общем случае никак не связано с «захватом» ни ресурсов компьютера (в первую очередь памяти), ни сетевых ресурсов.

Сервант

Итак, вы можете создать CORBA-объект и даже установить с ним связь. В общем случае этого совершенно недостаточно, чтобы использовать его в конкретной программе. Функциональность CORBA-объекта недоступна для клиента до тех пор, пока в программе (серверном приложении) не создан объект, который позволяет получить доступ к методам, объявленным в IDL-интерфейсе. Этот объект (реализованный на C++, Java, C, Cobol, Ada, Smalltalk или некоторых других языках) и называется «сервантом».

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

За время существования CORBA-объекта с ним может быть сопоставлено множество различных реализаций сервантов (но не более одного за раз). Более того, они могут содержаться в адресном пространстве различных приложений. Эти приложения могут быть даже запущены на различных компьютерах.

Часто говорят, что сервант является «инкарнацией» CORBA-объекта. Связь между сервантами и CORBA-объектами является хотя и строго формализованной, но очень гибкой. Сервант может быть создан раньше или позже CORBA-объекта; один сервант может «обслуживать» как один, так и несколько (иногда сотни тысяч и миллионы) CORBA-объектов. Явное разделение циклов жизни CORBA-объектов и их сервантов (а именно серванты потребляют реальные ресурсы) – один из столпов, на которых базируется очень высокая масштабируемость CORBA-приложений.

Объектная ссылка

Единственная сложность, связанная с пониманием смысла термина «объектная ссылка», состоит в том, что он используется в двух различных смыслах.

Есть объектная ссылка «мира CORBA», которая представляет собой закодированную информацию о CORBA-объекте. Она включает имя хоста, порта TCP/IP (или координаты Репозитария Реализаций), конечно же, уникальный идентификатор данного CORBA-объекта и множество другой информации, позволяющей клиенту установить связь с серверным объектом через границы языков программирования, операционных систем и аппаратных платформ. Операции с объектной ссылкой невозможны для клиента, за исключением того, что клиент может превратить ее в строку и записать в файл или базу данных. Впоследствии кто угодно может считать такую строку и преобразовать ее опять в объектную ссылку.

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

Концептуально переменная типа «объектная ссылка» является указателем на так называемый «proxy-объект», который существует на стороне клиента и обеспечивает выполнение удаленных вызовов. Сам proxy-объект сделан недоступным для программиста; связано это с тем, что его создание – задача не клиентского приложения, а самого ORB’а. Логически с каждым proxy-объектом сопоставлена отдельная объектная ссылка, и под копированием объектной ссылки следует понимать создание как нового proxy-объекта, так и настроенного на него нового «указателя». Разумеется, в реальных реализациях физического копирования proxy-объекта не происходит – как всегда в таких случаях, используется механизм счетчика ссылок.

Очень важно отчетливо понимать, что копирование (или уничтожение) объектных ссылок на стороне клиента влияет исключительно на клиентское приложение. Неправильное ведение счетчика ссылок в самом худшем случае приведет к продолжению физического существования в клиентском приложении ненужного proxy-объекта. Никакого отношения к серверному объекту эти действия не могут иметь в принципе. И создание, и уничтожение сервантов или серверных CORBA-объектов – задача серверного приложения. Философия CORBA состоит в том, чтобы клиент посылал сообщения «установить связь с существующим объектом» и «разорвать с ним связь», а не «создать серверный объект» и «уничтожить его». Разумеется, клиент может инициировать создание Corba-объектов вызвав у удаленного объекта специально предусмотренный для этого программистом (автором объекта) метод.