Разбирая автоматизацию OLE

Автор Бин Ли

Перевод Константин Лазарев

Введение

Хорошо известен факт, что для изучения и понимания технологии компонентной модели объектов Microsoft (COM - Component Object Model), требуется по меньшей мере несколько месяцев. Даже лучшие программисты и технологи в индустрии программного обеспечения знают это и приложили много усилий в последнии годы, чтобы упростить и донести до рядовых программистов (таких как Вы и я) все прелести и идеи COM. Тем не менее, даже при наличии большого количества имеющихся в данное время публкаций, приобретение опыта эффективного программирования COM или даже просто получение пользы от использования COM в Ваших приложениях требует значительных инвестиций, временных затрат и усилий.

Одной из причин больших затрат времени на изучение COM является тот факт, что COM включает в себя широкий спектр концепций и "суб-технологий", простирающихся от простейших органов управления ActiveX до "страшно запутанных" тем, таких как унифицированная передача данных (Uniform Data Transfer), связывание и встраивание объектов (Object Linking and Embedding), автоматизация (Automation), распределенная (Distributed) COM (DCOM) и т.д. В силу всего этого вовсе не очевидно какой путь изучения следует избрать для получения немедленной пользы от COM и использования его в Вашем бизнесе.

Очень простой и эффективный способ приступить к изучению COM состоит в том, чтобы начать с очень простой и, в то же время, очень мощной технологии COM, называемой Автоматизация OLE (OLE Automation) (или короче - автоматизация). Я говорю "простой" потому, что имеется масса документации по автоматизации, а также программ из реальной жизни, которые являются "готовыми к автоматизации" и которые могут служить прекрасными примерами для изучения. Я говорю "мощный" потому, что автоматизация является наиболее мощным методом, используемым сегодня для разработки промышленных подсистем (чаще подсистемы среднего уровня) для бизнеса (большинство из них любимы нами), работающих на платформе Windows. COM не только широко используется - она используется эффективно!

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

  1. Ввести Вас в технологию COM, если Вы никогда не пользовались ей, но желаете изучить ее.
  2. Разобрать и упростить автоматизацию таким образом, чтобы Вы могли использовать процесс изучения автоматизации (и COM в целом) позже при самостоятельном изучении оставшихся суб-технологий COM.

Автоматизация в двух словах

Автоматизация является очень хорошим примером эффективного использования старой идеи. Упрощая: если кто-то уже сделал это, то не стоит заново изобретать колесо, когда у Вас имеется возможность использовать имеющуюся функциональность к Вашей пользе. Механизм доступности функциональности для многократного использования ее снова и снова является основным в автоматизации (и COM в целом). Я должен подчеркнуть, что повторное (многократное) использование не означает простого повторного включения программного кода. Повторное использование означает также повторное межпрограммное использование функциональности с возможностью предоставления Вашим приложением своей функциональности другим приложениям. Повторное использование означает также языковую независимость функциональности. Например, скажем Вы написали некоторый функциональный блок на C++, а использовать его можно из программ, написанных на Delphi, Visual Basic, C++ и др.

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

Идея, заложенная в автоматизацию, включает разработку приложений, функциональность которых может быть доступна и другим приложениям, и разработку приложений, которые "знают" как использовать функциональность, предоставляемую Вам другими приложениями. Если говорить техническими словами, приложение, которое предоставляет некоторую повторно используемую функциональность называется сервером автоматизации (automation server) (также часто называемым сервером COM), а приложение, использующее функциональность, предоставляемую сервером автоматизации, называется клиентом автоматизации (automation client) (также часто называемым контроллером автоматизации). Важно подчеркнуть, что сервер автоматизации может не быть "чистым" сервером автоматизации, также как и клиент автоматизации может не быть "чистым" клиентом автоматизации. В действительности сервер автоматизации, использующий сервисы другого сервера автоматизации, является как сервером, так и клиентом одновременно. Клиент автоматизации, предоставляющий свои сервисы другому клиенту автоматизации, также является как клиентом, так и сервером автоматизации. Глубинные механизмы (сетевые и транспортные протоколы), с помощью которых клиент автоматизации взаимодействует с сервером, уже является частью собственно COM, что в свою очередь является основой для разработки клиент-серверных приложений, основанных на автоматизации/COM.

Внутри сервера автоматизации

Сервер автоматизации - это просто двоичный исполняемый модуль, который может состоять из нескольких объектов автоматизации. Объект автоматизации (также называемый объектом COM, хотя технически объект автоматизации является объектом COM особого сорта) - это отдельный, самодостаточный объект, спроектированный для выполнения специфической задачи или функции – очень похоже на "объект" в терминологии объектно-ориентированного программирования (ООП). В общем все объекты автоматизации, собранные в одном сервере автоматизации, предназначены для осуществления каких-то функциональных возможностей. Например, Microsoft Excel является сервером автоматизации, состоящим из нескольких меньших серверов автоматизации (Workbook - книга, Chart - диаграмма, Worksheet - лист, Range- диапазон и т.д.), каждый из которых представляет часть того, что Excel может делать. Идея заключается в том, что сервер автоматизации "позволяет" своим клиентам получать доступ и использовать свои объекты так же легко и просто, как-будто это его внутренние объекты. Следующий рисунок иллюстрирует эту идею:

Сервера автоматизации могут создаваться в виде DLL библиотек (иногда называемых внутренними серверами, так как DLL выполняется "в адресном пространстве клиента автоматизации") или в виде модулей EXE (иногда называемых внешними (out-of-process) серверами, так как EXE выполняются в отдельном/различном/"внешнем" процессе по отношению к клиенту автоматизации).

Некоторые преимущества создания DLL-серверов:

  1. DLL работают в одном процессе с клиентом и, следовательно, связь между клиентом и сервером оптимальна (наибольшее быстродействие).
  2. DLL обычно предпочитают при создании серверов, осуществляющих некоторую функциональность, связанную с пользовательским интерфейсом (User Interface). Нет необходимости создавать сервера в виде EXE-файлов, если Вы хотите создавать формы, окна, которые будут "наложены" (docked onto) или "пристроены" (plugged into) к клиентскому приложению.
  3. DLL сервера обычно более просты в отладке и тестировании, чем сервера EXE, которые потенциально придется отлаживать в удаленном режиме. Отладка и тестирование DLL серверов почти всегда производится на одной машине вместе с клиентским приложением.
  4. DLL сервера лучше приспособлены к ситуациям, когда архитектура с DLL является более удачной, например, при создании серверов, которым необходимо быть выполненными в виде DLL, например, для Web-серверов или при интеграции с Microsoft Transaction Server (MTS).

Некоторые преимущества создания серверов EXE:

  1. Сервера EXE более выгодны с точки зрения изоляции ошибок. При использовании серверов EXE, если клиент "валится", то это не производит неприятностей в сервере и наоборот, в случае DLL сервера, если клиент "валится", то вслед за ним "валится" и все, что связано с ним.
  2. Сервера EXE более подходящи в ситуации, когда распределенное решение и повышенная защищенность данных первичны.
  3. Сервера EXE более подходящи в ситуации, когда архитектура EXE-файла обязательна, например, при создании сервера, который должен работать в качестве сервиса Windows NT.

Сделав такое вступление, давайте рассмотрим простой сервер автоматизации на Delphi. Следующий фрагмент показывает реализацию объекта автоматизации, сгенерированную при создании библиотеки ActiveX (ActiveX Library) (File | New | ActiveX | ActiveX Library. Термин ActiveX - это просто маркетинговый термин Microsoft, но для наших целей и в нашем контексте мы определяем "ActiveX Library" как DLL сервер автоматизации/COM с именем MyServer. Затем создаем объект автоматизации (File | New | ActiveX | Automation Object) с именем MyObject.

unit MyObject;

interface

uses
  ComObj, ActiveX, MyServer_TLB;

type
  TMyObject = class(TAutoObject, IMyObject)
  protected
  end;

implementation

uses ComServ;

initialization
  TAutoObjectFactory.Create(ComServer, TMyObject, Class_MyObject,
    ciMultiInstance, tmApartment);
end.

В вышеприведенном примере мы видим реализацию на Delphi объекта автоматизации с именем MyObject, реализованного с помощью класса TMyObject. MyObject наследуется от класса TAutoObject (из ComObj.pas), который содержит базовые функции, общие для всех объектов автоматизации. TMyObject также поддерживает интерфейс IMyObject в качестве интерфейса автоматизации по умолчанию (или IDispatch, о нем немного позже). Не вдаваясь в детали, первичной целью IMyObject является "публикация" функциональности (методы, свойства, события и т.д.) объекта MyObject для внешнего мира таким образом, что клиенты автоматизации могут пользоваться объектом MyObject. Полное обсуждение основ интерфейсов в COM приводится в книге: David Chappell. Understanding ActiveX and OLE; ISBN 1-57231-216-5.

Мы узнали ранее, что автоматизация позволяет любому клиенту использовать объекты автоматизации не зависимо от того, на каком языке написан клиент. Принимая это во внимание, можно ожидать, что клиент, написанный на Visual Basic, создает экземпляр объекта MyObject следующим образом:


Dim oMyObject as object Set oMyObject = TMyObject.Create

Вышеприведенная строка является фикцией, так как TMyObject это внутренний класс Delphi и вызов TMyObject.Create является специфичной для Delphi формой записи, неопределенной в Visual Basic. Точно также, обобщая, Вы не можете вызывать TMyObject.Create ни из C++, ни из Java только потому, что такой синтаксис неприменим в этих языках. Как же тогда программа на VB, C++ или Java или какой-либо другой клиент автоматизации могут создать экземпляр объекта MyObject?

COM имеет концепцию объекта-"фабрики", чья первичная цель в жизни - это создание/соединение с обьектами автоматизации (COM), отсюда и термин "объект-фабрика". Механизм использования фабрик следующий:

На стороне сервера (Ваш сервер автоматизации):

  1. Когда Ваш сервер запускается, Вы создаете и "регистрируете" в COM фабрику. Фабрики обычно регистрируются явно в процессе инициализации сервера автоматизации. "Регистрация" в данном случае не подразумевает процесса записи своих точек входа в реестр. Подразумевается, что фабрика становится известной COM во время выполнения (и, в частности, клиенту) с помощью функции CoRegisterClassObject API.
  2. COM делает "зарегистрированную" фабрику доступной для использования Вашими клиентами.

На стороне клиента (Ваш клиент автоматизации):

  1. Когда Ваш клиент хочет создать серверный объект, Вы запрашиваете у COM информацию о специфической фабрике. Каждый создаваемый объект автоматизации/COM будет иметь ассоциированную фабрику.
  2. COM затем найдет затребованную фабрику в своем списке "зарегистрированных" фабрик и передаст ей управление.
  3. Затем Вы передаете запрос фабрике на создание экземпляра желаемого объекта автоматизации.

Заметьте: Шаги 1 и 2 на стороне сервера не являются необходимыми для серверов DLL. При разработке DLL серверов, COM предполагает, что Вы экспортируете функцию (DLLGetClassObject, но о ней немного позже), которую COM вызывает всякий раз, когда клиент затребует фабрику у Вашего сервера. В этом случае DLLGetClassObject выполняет все функции шагов 1 и 2. Также нет необходимости выполнять шаги 1, 2 и 3 на стороне клиента каждый раз, когда клиент желает создать объект COM. Вместо этого COM осуществляет вызов API, осуществляющий все шаги за один вызов функции.

Обращаясь вновь к нашему объекту MyObject, следует заметить, что Delphi вставил строку TAutoObjectFactory.Create в конце модуля. TAutoObjectFactory является реализацией фабрики в Delphi (также называется "фабрикой класса (class factory)" в COM), используемой для создания экземпляров объектов автоматизации. Заметьте, что одна фабрика обычно используется для обслуживания потребностей одного типа объектов, поэтому для каждого объекта автоматизации (унаследованного от TAutoObject), который Вы создаете в Delphi, обычно Вы видите (или Вам требуется) включить соответствующую строку TAutoObjectFactory.Create для этого объекта. Также важно отметить, что обычно имеется только один экземпляр фабрики (на экземпляр процесса сервера), который используется для создания любого числа объектов автоматизации/COM, с которыми ассоциирована эта фабрика.

TAutoObjectFactory.Create принимает несколько параметров, вот что они означают:

  1. TAutoObjectFactory.Create создает фабрику для объекта автоматизации MyObject таким образом, чтобы внешние клиенты могли создавать экземпляры MyObject.
  2. Второй параметр MyObject позволяет фабрике узнать, какой класс Delphi необходимо использовать при создании объекта MyObject.
  3. Третий параметр Class_MyObject позволяет фабрике публиковать себя в COM во время исполнения, используя уникальный идентификатор. Причина этому в том, что количество различных объектов автоматизации и серверов (и, следовательно, фабрик), которые могут одновременно исполняться, может быть любым, поэтому всем клиентам необходимо средство, позволяющее уникально идентифицировать каждую фабрику для них. Этот параметр называется глобальный уникальный идентификатор (Globally Unique Identifier - GUID), который гарантировано будет уникальным вне зависимости от времени и размещения. Редактор библиотеки типов Delphi использует COM для автоматической генерации значений GUID, Delphi автоматически использует дружественную программисту константу Class_MyObject для GUID (чтобы выяснить значение Class_MyObject, посмотрите файл MyServer_TLB.pas, автоматически создаваемый и сопровождаемый редактором библиотеки типов).
  4. Четвертый параметр ciMultiInstance определяет механизм, как используется фабрика для создания экземпляра MyObject. Тип данных для этого параметра TClassInstancing, он может принимать следующие значения:
  1. ciInternal - означает, что фабрика не публикует объект автоматизации для внешнего создания. Эта возможность может показаться немного странной, так как что может быть хорошего в объекте автоматизации, если он не может быть создан внешним клиентом? Цель этого значения - позволить серверу автоматизации быть способным внутренне создавать экземпляры объектов автоматизации (посредством фабрик), которые позже будут использоваться как "подобъекты" других объектов автоматизации (мы подробнее рассмотрим внутренние объекты и подобъекты позже в данной статье).
  2. ciSingleInstance - означает, что фабрика при запросе (посредством COM) создает не более одного экземпляра ассоциированного объекта автоматизации на один экземпляр сервера. Такое значение применимо только для серверов EXE. Каждый запрос клиента на создание второго экземпляра объекта автоматизации при использовании этого значения будет автоматически запускать второй экземпляр сервера автоматизации.
  3. ciMultiInstance - означает, что фабрика при запросе (посредством COM) создает столько экземпляров объекта автоматизации, сколько требует клиент, причем все экземпляры объектов автоматизации работают в одном экземпляре сервера. Это применимо только для серверов EXE.
  1. Пятый параметр tmSingle задает потоковую модель, которую поддерживает экземпляр MyObject. Проще говоря, потоковая модель говорит COM (и всему внешнему миру) как объект автоматизации может управлять многопоточностью. Если Вы не уверены какое значение должен иметь этот параметр для Ваших объектов автоматизации или Вы еще не думали о многопоточности Вашего сервера автоматизации, Вы должны использовать значение tmSingle, означающее, что Ваш объект автоматизации может быть безопасно использован только в контексте одного потока. Другие значения для этого параметра: tmApartment, означающее, что Ваш объект автоматизации использует apartment threading - работу в одном подразделении; tmFree, означающее, что Ваш объект автоматизации поддерживает free threading - многопотоковую работу; tmBoth, означающее, что Ваш объект автоматизации поддерживает оба типа, как работу в одном подразделении, так и многопотоковую работу. Для получения более подробной информации о потоках в COM обратитесь к другой моей статье "COM Threading Using Delphi".

Фабрика позволяет создавать свой ассоциированный объект автоматизации/COM поддержкой интерфейса IClassFactory. IClassFactory определяется следующим образом:

IClassFactory = interface(IUnknown)
  ['{00000001-0000-0000-C000-000000000046}']
  function CreateInstance(const unkOuter: IUnknown; const iid: TIID;
    out obj): HResult; stdcall;
  function LockServer(fLock: BOOL): HResult; stdcall;
end;

Не вдаваясь во многие детали IClassFactory.CreateInstance предоставляет механизм, посредством которого клиент может просить фабрику создать экземпляр объекта COM. Мы изучим более детально как в действительности клиент использует фабрику для создания экземпляров объектов COM несколько позже в этой статье.

Итак, мы увидели, как мы можем легко создавать объекты автоматизации в Delphi, используя TAutoObject и как мы можем сделать объекты автоматизации доступными для клиентов, используя TAutoObjectFactory. Однако мы не увидели, как мы можем получить какие-либо полезные функции от наших объектов автоматизации, чтобы клиенты могли ими воспользоваться. Это происходит посредством интерфейса IMyObject.

Нормой для объектов автоматизации является поддержка интерфейса автоматизации по умолчанию, раскрывающего все его функции. Этот интерфейс обычно наследуется от IDispatch, который обязательно требуется COM для осуществления "способности к автоматизации (automation capability)". Используя этот интерфейс, Вы можете получить доступ к методам и свойствам, позволяющим клиентам Вашего объекта автоматизации получить доступ ко всей функциональности. Например, возвращаясь к MyObject, мы можем добавить метод (используя редактор библиотеки типов) под названием DoSomething к IMyObject и затем реализовать TMyObject.DoSomething следующим образом:


type
  TMyObject = class(TAutoObject, IMyObject)
  protected
    procedure DoSomething; safecall;
  end;

implementation

procedure TMyObject.DoSomething;
begin
  ShowMessage ('MyObject just did something!');
end;

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

var v : variant;
v := Create an instance of MyObject;
v.DoSomething;

Из этого очень простого примера теперь очень просто увидеть возможности того, что может быть реализовано в объекте автоматизации. Вы можете создать множество объектов, начиная со складывающих два числа до обрабатывающих данные реального времени для сотен клиентов Ваших Web-серверов. Прекрасное качество Ваших объектов заключается в том, что они независимы от языка (reusable across languages) и они могут существовать где угодно в Вашей сети (can exist anywhere on your network).

Внутри клиента автоматизации

Клиент автоматизации - это приложение, способное извлекать пользу из сервисов и функциональности, заключенных в сервере автоматизации. Клиент автоматизации может быть EXE-приложением, модулем DLL и даже script-файлом, способным выполняться с использованием виртуальной машины (интерпретатора скрипта), такой как, например, Active Server Page (ASP) от Microsoft.

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

  1. Сперва клиент запрашивает у COM (во время исполнения) фабрику, ассоциированную с объектом автоматизации, который он желает создать.
  2. COM производит поиск в системе (загружает и запускает сервер автоматизации, если это необходимо) для затребованной фабрики и возвращает ее клиенту, если поиск произошел успешно.
  3. Клиент затем может использовать фабрику для создания экземпляра объекта автоматизации (с использованием метода IClassFactory.CreateInstance).

Вам может показаться, что весь процесс выглядит перевернутым вверх ногами, и это только для создания экземпляра объекта автоматизации. На практике этот процесс очень эффективен, потому как он предоставляет доступ с любых языков программирования для создания экземпляров объектов автоматизации/COM любых типов унифицированным и последовательным способом. Для углубленного изучения обратитесь к David Chappell - Understanding ActiveX and OLE и Dale Rogerson - Inside COM; ISBN 1-572-31349-8 (имется перевод на русский язык: Дейл Роджерсон - Основы COM; ISBN 5-7502-0033-7).

Для того, чтобы предоставить клиентам доступ к фабрике, COM имеет функцию CoGetClassObject:

function CoGetClassObject (const clsid: TCLSID; dwClsContext: Longint;
  pvReserved: Pointer; const iid: TIID; out pv): HResult; stdcall;

CoGetClassObject принимает несколько параметров, ниже приведено их назначение:

  1. Первый параметр clsid используется для идентификации фабрики, для которой делается запрос клиентом. В нашем примере этот параметр должен быть установлен равным Class_MyObject - уникальному GUID, идентифицирующему конкретный TAutoObjectFactory для TMyObject, как уже говорилось ранее.
  2. Второй параметр dwClsContext специфицирует контекст исполнения фабрики (и COM-объекта), которым интересуется клиент. Не вдаваясь в технические детали, в качестве этого параметра может быть использована константа CLSCTX_SERVER (ActiveX.pas).
  3. Четвертый параметр iid, каким интерфейсом фабрики интересуется клиент. Как было упомянуто выше, клиент интересуется фабрикой для того, чтобы создать экземпляр обхекта автоматизации/COM и, следовательно, передача IClassFactory в качестве этого параметра есть то, что наиболее интересует клиента. Позднее он может вызвать IClassFactory.CreateInstance.
  4. Пятый параметр pv является выходным параметром, который CoGetClassObject использует для возврата указателя на интерфейс затребованной фабрики.

Получив однажды успешно интерфейс IClassFactory фабрики, клиент может вызвать затем IClassFactory.CreateInstance для создания экземпляра желаемого объекта COM.

function IClassFactory.CreateInstance(const unkOuter: IUnknown; 
  const iid: TIID; out obj): HResult; stdcall;

IClassFactory.CreateInstance принимает несколько параметров и ниже приведено их описание:

  1. Первый параметр unkOuter имеет отношение к указателю на "объект-собственник (owner-object)". Используется при создании экземпляров объектов COM, которые спроектированы с возможностью агрегирования, или являются "подобъектами (sub-objects)" других объектов COM. На данный момент нас этот параметр не интересуют в деталях, и мы можем просто установить его в NIL.
  2. Второй параметр iid специфицирует какой из интерфейсов вновь созданного объекта хочет получить клиент. В нашем примере мы передаем IMyObject в качестве этого параметра, так как мы хотим получить методы интерфейса IMyObject, лежащие на поверхности функциональных возможностей объекта MyObject.
  3. Третий параметр obj - это выходной параметр, который CreateInstance использует для возврата указателя на затребованный клиентом интерфейс, задаваемый параметром iid.

В соответствии с вышесказанным следующий пример демонстрирует, как клиент на Delphi может создать экземпляр объекта MyObject и вызвать его метод DoSomething.

uses
  Windows,
  MyServer_TLB,
  ActiveX,
  ComObj;

procedure TForm1.Button1Click(Sender: TObject);
var
  hr : HResult;
  pMyObjectFactory : IClassFactory;
  pMyObject : IMyObject;
begin
  // получаем фабрику объекта MyObject (интерфейс IClassFactory)
  hr := CoGetClassObject (Class_MyObject, CLSCTX_SERVER, NIL, 
                                      IClassFactory, pMyObjectFactory);
  OleCheck (hr);

  // запрашиваем фабрику объекта MyObject для создания экземпляра MyObject
  hr := pMyObjectFactory.CreateInstance (NIL, IMyObject, pMyObject);
  OleCheck (hr);

  // теперь у нас есть экземпляр объекта MyObject, запускаем метод DoSomething
  // этого экземпляра! 
  pMyObject.DoSomething;
end;

Заметьте, что в вышеприведенном примере я использую процедуру OleCheck (ComObj.pas) для проверки состояния после вызова каждой, связанной с COM, операции. Почти все COM API или методы интерфейса COM возвращают код своего завершения HResult (longint). Цель вызовов OleCheck - гарантировать, что в случае возврата ошибочного кода завершения HResult, немедленно будет сгенерирована исключительная ситуация с указанием вида ошибки, а дальнейшее исполнение (которое может привести к непредсказуемым результатам) будет прервано. После этого объяснения я настоятельно рекомендую использовать процедуру OleCheck в Ваших программах, чтобы не заниматься поиском потенциальных ошибок, которые могут быть выявлены простыми вызовами OleCheck.

В дальнейшемВы поймете, что CoGetClassObject необходимо вызывать после IClassFactory.CreateInstance каждый раз, когда нам понадобится получить доступ к объекту COM. COM предлагает также и другой вариант - вызов функции API CoCreateInstance, который упрощает 2 вызова, заменяя их одним, что более удобно в использовании в случае, когда необходимо получить доступ ко множеству объектов различных типов в одной сессии:

function CoCreateInstance(const clsid: TCLSID; unkOuter: IUnknown;
  dwClsContext: Longint; const iid: TIID; out pv): HResult; stdcall;

CoCreateInstance использует несколько параметров и ниже приведено их описание:

  1. Первый параметр clsid используется для указания, какую фабрику собираются использовать для создания экземпляра желаемого объекта COM.
  2. Второй параметр unkOuter имеет отношение к указателю на "объект-собственник (owner-object)". Используется при создании экземпляров объектов COM, которые спроектированы с возможностью агрегирования, или являются "подобъектами (sub-objects)" других объектов COM. На данный момент нас этот параметр не интересуют в деталях, и мы можем просто установить его в NIL.
  3. Третий параметр dwClsContext специфицирует контекст исполнения фабрики (и COM-объекта), которым интересуется клиент. Не вдаваясь в технические детали, в качестве этого параметра может быть использована константа CLSCTX_SERVER (ActiveX.pas).
  4. Четвертый параметр iid специфицирует какой из интерфейсов вновь созданного объекта хочет получить клиент. В нашем примере мы передаем IMyObject в качестве этого параметра, так как мы хотим получить методы интерфейса IMyObject, лежащие на поверхности функциональных возможностей объекта MyObject.
  5. Пятый параметр pv является выходным параметром, который CoCreateInstance использует для возврата указателя на интерфейс, специфицированный параметром iid.

Возвращаясь к нашему примеру, CoCreateInstance может быть использован следующим образом:

uses
  Windows,
  MyServer_TLB,
  ActiveX,
  ComObj;

procedure TForm1.Button1Click(Sender: TObject);
var
  hr : HResult;
  pMyObject : IMyObject;
begin
  // предыдущие шаги больше не нужны из-за использования CoCreateInstance
  hr := CoCreateInstance (Class_MyObject, NIL, CLSCTX_SERVER, 
                                       IMyObject, pMyObject);
  OleCheck (hr);

  // теперь у нас есть экземпляр объекта MyObject, запускаем метод DoSomething
  // этого экземпляра! 
  pMyObject.DoSomething;
end;

Но если Вы хотите использовать что-то такое, что проще запомнить и имеет встроенную поддержку проверки кодов завершения вместо OleCheck, Вы можете использовать функцию CreateComObject (ComObj.pas), поставляемую в составе Delphi:

uses
  MyServer_TLB,
  ComObj;

procedure TForm1.Button1Click(Sender: TObject);
var
  pMyObject : IMyObject;
begin
  // проще выглядящая CoCreateInstance с использванием функции Delphi 
  // CreateComObject
  pMyObject := CreateComObject (Class_MyObject) as IMyObject;

  // теперь у нас есть экземпляр объекта MyObject, запускаем метод DoSomething
  // этого экземпляра! 
  pMyObject.DoSomething;
end;

Вы еще не любите людей из Inpise, всегда делающих жизнь проще для тех из нас, кто является программистом на Delphi?

Заметьте, что в вышеприведенном примере производится приведение типов возвращаемого функцией CreateComObject значения к IMyObject с использованием оператора as только потому, что CreateComObject внутри себя вызывает CoCreateInstance, запрашивая указатель на интерфейс IUnknown (как она может знать, что мы хотим получить IMyObject, если мы даже не передаем IMyObject в CreateComObject?). Если Вы не знакомы с оператором as в данном контексте, то он используется для получения желаемого интерфейса (IMyObject в нашем случае) через посредство другого интерфейса (IUnknown в нашем случае) путем неявного вызова IUnknown.QueryInterface и, в то же время, производя проверку правильности результата выполнения IUnknown.QueryInterface.

Я хотел бы еще продемонстрировать Вам следующий фрагмент текста из файла MyServer_TLB.pas, который автоматически генерируется редактором библиотеки типов Delphi:

type
  …
  CoMyObject = class
    class function Create: IMyObject;
    class function CreateRemote(const MachineName: string): IMyObject;
  end;
  …
class function CoMyObject.Create: IMyObject;
begin
  Result := CreateComObject(CLASS_MyObject) as IMyObject;
end;

Теперь Вы хорошо знаете, что это означает: просто использование класса CoMyObject для создания экземпляров MyObject, т.е.

uses
  MyServer_TLB;

procedure TForm1.Button1Click(Sender: TObject);
var
  pMyObject : IMyObject;
begin
  // простейший метод создания MyObject, используя 
  // объявление компонентного класса CoMyObject
  pMyObject := CoMyObject.Create;
  pMyObject.DoSomething; 
end;

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

Заметьте, что все примеры, которые мы видели в данной статье, осуществляют прямые манипуляции с интерфейсом IMyObject вместо того, чтобы вызывать IMyObject.DoSomething. В терминологии COM такой процесс "прямого связывания (directly binding)" с IMyObject обычно трактуется как раннее связывание. Другими словами раннее связывание - это процесс запуска сервисов объектов COM с использованием определенного, установленного во время компиляции вызова, такого, например, как вызов методов класса, созданного внутри Delphi. Не беспокойтесь, если Вы удивлены этим определением: мы узнаем несколько больше о раннем связывании позже в этой статье.

Интерфейс IDispatch

До сих пор я рассматривал вопрос, что из себя представляет IDispatch и где он используется при разработке объектов автоматизации и серверов. Прежде, чем погрузиться в детали IDispatch, давайте попытаемся понять, зачем он нужен.

Основная идея IDispatch заключается в том, что где-то должен быть механизм, позволяющий клиентам автоматизации различных типов доступаться к методам объектов автоматизации простым, совместимым и прямо ведущим к цели способом. Множество клиентов автоматизации, в частности, основанные на скриптах, таких как VBScript или JavaScript, не поддерживают манипуляции с массивами указателей на интерфейсы (использование раннего связывания) при доступе к объектам автоматизации. Давайте рассмотрим пример.

Следующий пример показывает как клиент использует скрипт Visual Basic for Applications (VBA) для создания экземпляра MyObject и вызова MyObject.DoSomething:

Dim vMyObject as Object
Set vMyObject = CreateObject ("MyServer.MyObject")
vMyObject.DoSomething

Заметьте, что этот скрипт будет исполняться с помощью интерпретатора скрипта (или на виртуальной машине) и, следовательно, интерпретатор, очевидно, будет нуждаться в получении доступа к интерфейсу IMyObject объекта MyObject для исполнения vMyObject.DoSomething (DoSomething является методом IMyObject). Далее, этот же самый интерпретатор скрипта также должен иметь возможность выполнять все (и любые) запросы создания объектов автоматизации и их методы, что требует от интерпретатора знания всех возможных в мире интерфейсов (IThisObject - IЭтотОбъект, IThatObject -IТотОбъект, IWhatnotObject - IЧтоНеОбъект и т.д.), которые мы только могли бы придумать. Очевидно, что такой интерпретатор спроектировать невозможно, и, даже если бы это было возможно, его невозможно было бы использовать практически. Для решения этой проблемы введена концепция "позднего связывания".

Говоря простыми словами, позднее связывание является особенностью, позволяющей клиентам COM доступаться к методам объектов COM без предварительного знания (или даже необходимости знать) о действительном интерфейсе COM, определяющем методы. Другими словами, позднее связывание позволяет клиентам, работающим на скриптах, вызывать vMyObject.DoSomething без необходимости использования интерпретатором интерфейса IMyObject. IDispatch и есть тот механизм, который в основном обеспечивает позднее связывание.

IDispatch определяется следующим образом:

IDispatch = interface(IUnknown)
  ['{00020400-0000-0000-C000-000000000046}']
  function GetTypeInfoCount(out Count: Integer): HResult; stdcall;
  function GetTypeInfo(Index, LocaleID: Integer; out TypeInfo): HResult;
    stdcall;
  function GetIDsOfNames(const IID: TGUID; Names: Pointer;
    NameCount, LocaleID: Integer; DispIDs: Pointer): HResult; stdcall;
  function Invoke(DispID: Integer; const IID: TGUID; LocaleID: Integer;
    Flags: Word; var Params; VarResult, ExcepInfo, ArgErr: Pointer): HResult;
    stdcall;
end;

Наиболее важными методами интерфейса IDispatch, используемыми для позднего связывания, являются IDispatch.GetIDsOfNames и IDispatch.Invoke. Идея того, как реализуется позднее связывание посредством IDispatch, состоит в следующем:

  1. Как уже упоминалось ранее каждый объект автоматизации нуждается в определении интерфейса автоматизации по умолчанию, и этот интерфейс должен поддерживать интерфейс IDispatch (или наследоваться от него). Это объясняет то, почему родительским интерфейсом IMyObject (в редакторе библиотеки типов) является IDispatch.
  2. Каждый метод этого интерфейса по умолчанию должен иметь уникальный номер (не обязательно последовательные номера, они должны быть уникальны для интерфейса). В контексте позднего связывания и интерфейса IDispatch этот уникальный номер назначается каждому методу и называется Dispatch ID (в краткой записи dispid).
  3. В интерфейсе по умолчанию должен быть корректно реализован IDispatch.GetIDsOfNames, т.е. он должен обеспечивать механизм трансляции имен методов в их идентификаторы (dispid). Другими словами, необходимо обеспечить клиенту возможность спросить "какой dispid имеет метод Method1, какой dispid имеет метод Method2, какой dispid имеет метод Method10 и т.д.". При этом необходимо обеспечить получение соответствующего dispid на каждый запрос.
  4. Интерфейс по умолчанию также должен правильно реализовывать IDispatch.Invoke, т.е. необходимо обеспечить соответствующий механизм вызова методов, заданных посредством только dispid и передачи массива необходимых методу параметров. Другими словами, необходимо предоставить клиенту возможность сказать: "Вызови метод, чей dispid = 1, используя этот список параметров". А он в свою очередь должен корректно разобрать список параметров и вызвать затребованный метод, передав ему соответствующие значения параметров.

Учитывая приведенные требования, теперь можно очень легко понять, как интерпретатор может пользоваться только IDispatch для работы с любыми типами объектов автоматизации. Например, следующие шаги показывают как интерпретатор исполняет наш вызов vMyObject.DoSomething:

  1. Сначала интерпретатор запрашивает интерфейс автоматизации по умолчанию (IDispatch) объекта IMyObject. В нашем случае интерпретатор получает интерфейс IMyObject (IMyObject является нашим интерфейсом по умолчанию, поддерживающим IDispatch). Отметим, чтобы не сбивать Вас с толку: интерпретатор ничего не знает о IMyObject. Все, что он знает - это то, что интерфейс по умолчанию IDispatch, так уж случилось, поддерживается посредством IMyObject.
  2. Затем интерпретатор, используя интерфейс по умолчанию IDispatch, запрашивает dispid метода DoSomething. Это производится вызовом IDispatch.GetIDsOfNames.
  3. Получив dispid для DoSomething, интерпретатор затем вызывает IDispatch.Invoke, передавая dispid метода DoSomething и массив параметров, который пуст для DoSomething, так как параметров нет. Интерпретатор ожидает, что MyObject правильно реализует вызов IDispatch.Invoke и внутренне вызывает соответствующий метод, основываясь на dispid метода DoSomething и параметрах.

В мире автоматизации четыре ступени, которые я описал, определяют термин "позднее связывание". Если Вы следили внимательно, метод позднего связывания включает в себя:

  1. Запрос у объекта автоматизации интерфейса по умолчанию IDispatch.
  2. Далее следует вызов IDispatch.GetIDsOfNames
  3. Далее следует вызов IDispatch.Invoke

Это противоположно тому, что происходит при "раннем связывании", которое включает в себя:

  1. Запрос у объекта автоматизации желаемого интерфейса (IMyObject)
  2. Далее следует прямой вызов (ранне связывание) желаемого интерфейса (IMyObject.DoSomething)

Следующие наблюдения описывают различия между поздним и ранним связыванием:

  1. Позднее связывание требует знания только интерфейса IDispatch, тогда как раннее связывание требует знания всего списка необходимых интерфейсов. Другими словами, позднее связывание очень удобно использовать в "скриптовых" окружениях, когда просто невозможно "знать" все возможные интерфейсы до времени исполнения - интерпретаторам необходимо знать только то, как осуществляются вызовы IDispatch.
  2. Метод позднего связывания включает больше шагов, трансляций и тратится существенно больше времени, чем при раннем связывании. Это объясняет, почему позднее связывание в общем медленнее, чем раннее связывание. В действительности я рекомендую использовать позднее связывание только в том случае, если раннее связывание неосуществимо вообще или осуществление вызовов раннего связывания существенно менее удобно, чем эквивалентные вызовы позднего связывания. Во всех остальных случаях следует пользоваться ранним связыванием.

Следующий фрагмент показывает вариант позднего связывания, реализованный в Delphi, который создает объект MyObject и вызывает DoSomething:

uses
  MyServer_TLB,
  ActiveX,
  ComObj;

procedure TForm1.Button2Click(Sender: TObject);
const
  // представляет из себя пустой массив параметров
  dpNoArgs : TDispParams = (
    rgvarg : NIL; rgdispidNamedArgs: NIL; cArgs : 0; cNamedArgs: 0
  );
var
  pMyObject : IDispatch;
  sMethodName : widestring;
  iDispId : longint;
begin
  // позднее связывание с MyObject с использованием IDispatch
  pMyObject := CreateComObject (Class_MyObject) as IDispatch;

  // вызов GetIDsOfNames для получения dispid метода DoSomething в iDispId
  sMethodName := 'DoSomething';
  OleCheck (pMyObject.GetIDsOfNames (GUID_NULL, @sMethodName, 1,
    LOCALE_SYSTEM_DEFAULT, @iDispId));

  // вызов Invoke с использованием iDispId метода DoSomething
  OleCheck (pMyObject.Invoke (iDispId, GUID_NULL, LOCALE_USER_DEFAULT,
    DISPATCH_METHOD, dpNoArgs, NIL, NIL, NIL));
end;

Я не хочу вдаваться в детали передаваемых в IDispatch.GetIDsOfNames и IDispatch.Invoke параметров (они обсуждаются в последнем разделе), но вышеприведенный пример ясно показывает те шаги, которые необходимо выполнить для осуществления вызова с использованием позднего связывания. Очевидно, что для осуществления множества вызовов методов при позднем связывании, приходится проделывать множество вызовов IDispatch.GetIDsOfNames и IDispatch.Invoke. В этой связи люди из Inprise упростили это, доверив компилятору Delphi сделать все необходимые вызовы IDispatch за Вас. Вот пример:

uses
  MyServer_TLB,
  ActiveX,
  ComObj;

procedure TForm1.Button2Click(Sender: TObject);
var
  vMyObject : variant;
begin
  // упрощение вызовов позднего связывания, доверяя обслуживание
  // вызовов GetIDsOfNames и Invoke компилятору Delphi
  vMyObject := CreateComObject (Class_MyObject) as IDispatch;
  vMyObject.DoSomething;
end;

Здесь нет никакого волшебства: просто компилятор генерирует некий код "за сценой", который производит все неоходимые вызовы IDispatch при выполнении vMyObject.DoSomething. Если бы компилятор Delphi был бы скрипт-интерпретирующим, то он производил бы в точности те же действия, что и интерпретатор Visual Basic for Applications, пример программы на котором мы видели ранее.

IDispatch имеет еще 2 метода, которые мы еще не обсуждали. Не вдаваясь сейчас в какие-либо детали, отметим, что IDispatch.GetTypeInfoCount и IDispatch.GetTypeInfo в основном работают с информацией о типах, получаемой от объектов автоматизации. Информация о типе - это только технический термин для "описания" данного интерфейса автоматизации в том смысле, что информация о типе позволяет клиентам запрашивать информацию об интерфейсе автоматизации, такую как: сколько медотов он имеет, какие имена имеют методы, какие параметры имеет каждый метод и т.д. Информация о типе управляется интерфейсом ITypeInfo (другой интерфейс COM). Мы увидим и другие детали этого интерфейса позже в данной статье.

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

  1. CreateOleObject - это функция Delphi, а не COM API.
  2. CreateOleObject внутри себя вызывает CoCreateInstance для построения объекта COM. CreateOleObject получает строковый идентификатор (технически Programmatic ID или ProgId) и поэтому есть необходимость каким-то образом преобразовать ProgId в ClassId, как это необходимо для вызова CoCreateInstance.
  3. CreateOleObject внутри себя вызывает функцию ProgIdToClassId для преобразования ProgId в ClassId. Преобразование ProgId в ClassId происходит через поиск в системном реестре.

Идея, кроющаяся в использовании ProgId, заключается в способе идентификации компонентных классов (CoClasses) (об уникальных идентификаторах фабрик классов упоминалось выше) с использованием дружественного пользователю имени в виде строки вместо использования ClassId (который на самом деле является GUID). Эта строка обычно имеет форму <Server Name.Object Name> и определяется точкой входа в библиотеке типов. ProgId необходимы только для тех клиентских окружений (client environments), которые не позволяют работать с неприличными GUID. Другими словами ProgId - это просто удобная форма предоставления возможности работы с COM объектами для определенных реализаций клиентов.

Следующий фрагмент демонстрирует как CreateOleObject используется совместно с вызовом позднего связывания:

uses
  ActiveX,
  ComObj;

procedure TForm1.Button2Click(Sender: TObject);
var
  vMyObject : variant;
begin
  // CreateOleObject и вызов позднего связывания
  vMyObject := CreateOleObject ('MyServer.MyObject');
  vMyObject.DoSomething;
end;

Заметьте, что "MyServer.MyObject" происходит от имени нашего сервера "MyServer" и имени объекта MyObject, как это определено в библиотеке типов.

Delphi позволяет Вам работать и с неприличными GUIDами (и, следовательно, с ClassId), что означает, что если у Вас имеется доступ к ClassId, то нет необходимости в использовании CreateOleObject. Delphi предоставляет в Ваше распоряжение утилиту для импорта библиотеки типов (Project | Import Type Library или использование утилиты TLibImp.exe) для генерации файла "интерфейса библиотеки типов" (xxx_TLB.pas) почти для любых серверов COM. Этот файл предоставляет доступ ко всем GUIDам, компонентным классам (CoClasses), определениям интерфейсов (Interface definitions) и т.д., которые имеются в сервере. Вы можете легко "связаться" с этим файлом и использовать определения компонентных классов (CoClass) для создания экземпляров объектов COM.

Rambler's Top100