Автор: Роман Панышев (irrona)


Практическое использование IDispatch

Статья первая

      Этой статьей я начинаю серию статей о практическом использовании так называемого диспетчерского интерфейса IDispatch для работы с COM-объектом. Содержимое статей ориентированно в основном на практиков-программистов, которые уже знакомы с компонентной моделью и знают как создавать COM-сервера. В любом случае я, по возможности, буду подробно останавливаться на вопросах создания COM-объектов (для этого буду использовать IDE Visual Basic версии 6.0, т.к. на мой взгяд это наиболее простая среда разработки для создания таких компонентов). Поэтому, я думаю, статья может заинтересовать и менее искушенных в таких вопросах читателей. Для доступа к интерфейсам COM-объектов я буду использовать пакет masm32 версии 8.2 Стивена Хатчессона (для тех, кто еще не скачал вот ссылка. Кстати я не делаю различия между понятиями COM-объект и COM-сервер. Поэтому и Вы, встречая их тексте, знайте, что я имел в виду одно и то же.

      В начале было слово

      Под COM-объектами обычно понимают целый ряд программных объектов, которые можно разбить на две основные группы: Inproc-сервера, выполняющиеся в адресном пространстве вызывающего процесса (в основном это файлы с расширением dll), и Outproc-сервера, выполняющиеся в отдельном (или собственном) адресном пространстве (файлы с расширением exe). Кроме этого Outproc-сервера могут запускаться как локально, так и удаленно (т.е. физически вызывающая программа-клиент и COM-сервер находятся на разных машинах в сети). Для более детального ознакомления с понятием COM я рекомендую прочитать две, на мой взгляд, замечательные книги, посвященные COM: "Основы COM" автор Дейл Роджерсон и "Сущность технологии COM" автор Дональд Бокс.

      Если Вы уже какое-то время работали с пакетом masm32, то наверное обратили внимание на то, что в нем присутствует папка com, в которой имеются документация и целый набор включаемых файлов и библиотек, упрощающих работу с COM, используя ассемблер. Но в этих примерах делается упор на использование так называемого неопределенного интерфейса IUnknown. Для примеров, которые будут описанны в данной статье использование IUnknown тоже подходит, но я хотел бы показать более простой путь. Почему именно интерфейс IDispatch? Да потому, что IDispatch был разработан как способ управления объектом с помощью Автоматизации (в прошлом это называлось OLE Автоматизацией). Данный способ применяется такими программными продуктами фирмы Microsoft как MSWord, MSExcel, MSAccess, которые имеют встроенную поддержку так называемого Visual Basic For Application (сокращенно VBA). Этот же способ широко используют программисты на "чистом" Visual Basic, Java, FoxPro и др. Кроме того, скриптовые языки VBScript и JavaScript используют Автоматизацию для доступа к COM-объектам. А они, как Вам известно, широко используются для написания как самостоятельно работающих модулей с расширением *.vbs и *.js, так и сценариев для WEB-страниц. Но что хорошо для программистов, например на VB, то плохо для тех, кто программирует на С++. Не буду утверждать, т.к. не являюсь С-шником, но по слухам программисты на С++ предпочитают использовать интерфейс IUnknown для прямого доступа к COM-объектам. А так как программистам на других высокоуровневых языках использовать IUnknown намного сложнее, фирма Microsoft и разработала дополнительный интерфейс IDispatch и позволила создавать COM-объекты с дуальным интерфейсом, содержащими в себе как IUnknown, так и IDispatch. Теперь использование COM-объекта, например, на VB стало очень простым и прозрачным для программиста, поскольку компилятор скрывает от него весь процесс инициализации и вызовов внутренних методов COM-объекта. Например, используя ранее связывание, это выглядит так:

Sub SomeFunction()
	Dim obj As mycom.myclass
	Set obj = New mycom.myclass
	obj.mymethod()
	Set obj = Nothing
End Sub

      А с помощью позднего связывания, так:

Sub SomeFunction()
	Dim obj
	Set obj = CreateObject("mycom.myclass")
	obj.mymethod()
	Set obj = Nothing
End Sub

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

Unknown интерфейс Dispatch интерфейс
рис.1 Интерфейс IUnknown рис.2 Интерфейс IDispatch

      Собственно, у интерфейса IUnknown есть три указателя на интерфейсы, являющиеся основой COM и позволяющие начать работу с COM-объектом: QueryInterface (служит для запроса у объекта указателей на другие интерфейсы), AddRef (служит для увеличения счетчика ссылок на интерфейс) и Release (служит для уменьшения счетчика ссылок на интерфейс). IDispatch включает в себя эти же три основных указателя интерфейсов, плюс четыре собственных указателя интерфейсов, главным из которых для нас является Invoke, т.к. благодаря ему у нас появится возможность вызывать на выполнение методы, реализованные в COM-объекте, не намного сложнее, чем на языке высокого уровня.

      Что такое указатель на интерфейс? Для нас это не более, чем вызов функции COM-объекта. То есть, каждый указатель указывает нам на местоположение вызываемой функции в COM-объекте. Так как каждый указатель имеет размер 4 байта (dword), то простым смещением от указателя на IDispatch, можно вызвать функцию, реализованную в объекте. Но мы люди интеллигентные и поэтому будем использовать непосредственно названия функций. Так проще и понятнее.

      Внутренние локальные сервера

      Итак, приступим. Для начала создадим простейший COM-объект. Напишем Inproc-сервер в виде dll, который будет зарегестрирован на одной машине с клиентом. Для этого запускаем Visual Basic из пакета Microsoft Visual Studio 6.0. В качестве нового проекта выбираем ActiveX Dll. Переходим в окно Project Explorer и даем нашему проекту название mycom, а Class1 переименовываем в myclass. В окне нашего класса пишем следующий код:

Public Sub mymethod()
    MsgBox "Hello"
End Sub
mycom

рис.3 Создание COM-сервера

      Теперь сохраняем проект и выбираем в меню File пункт "Make mycom.dll". После компиляции Visual Basic сам беспокоится за нас о регистрации полученного COM-объекта в реестре Windows. Запомните это на будущее! Таким образом у нас получился простейший Inpoc-сервер с дуальным (dual) интерфейсом. Для просмотра сведений об объекте можно воспользоваться утилитой OleView из поставки Visual Studio. Для этого запускаем утилиту, раскрываем ветку "All objects" и на ходим наш объект, который назвается mycom.myclass. Кликнув правой кнопкой мыши на названии объекта, выберите из контекстного меню пункт View Type Information.... OleView сгенерирует idl-файл для нашего COM-объекта, в котором можно более детально ознакомиться с характеристиками объекта. В категории описания интерфейса myclass можно увидеть выставленные флаги dual (что говорит о двойственности нашего объекта) и oleautomation (наш объект является объектом OLE). Далее видно, что доступ к интерфейсу myclass осуществляется через интерфейс IDispatch и в нем присутствует единственный заданный нами метод mymethod (в нашем случае пока без параметров). Для получения доступа к объекту (или, иначе говоря, к IDispatch), нам необходим либо его CLSID, представляющий из себя уникальный (глобальный) идентификатор - GUID - в виде {00000000-0000-0000-0000-000000000000}, либо ProgID - в данном случае он будет mycom.myclass.

      Теперь привожу полный код программы на ассемблере для доступа к нашему COM-объекту. Все объяснения после.

.586
.model flat, stdcall
option casemap :none

	include	 windows.inc
	include kernel32.inc
	include user32.inc
	include ole32.inc
	include oaidl.inc
	include L.inc

	includelib kernel32.lib
	includelib user32.lib
	includelib ole32.lib

	main proto
	dowork proto
	
.data
	pIDispath		dd 0
	rclsid			dd 0
	dispid			dd 0
	lcid			dd 0
	lpProgID		wchar L(<mycom.myclass\0>)
	fn			dd offset FuncName
	FuncName		dw 'm','y','m','e','t','h','o','d',0,0,0
	IID_NULL		GUID <0,0,0,<0,0,0,0,0,0,0,0>>
	IID_IDispatch		GUID <000020400H,00000H,00000H,<0C0H,000H,000H,000H,000H,000H,000H,046H>>

.data?
	dsppar			DISPPARAMS <?>
	
.code
start:
main proc
	invoke dowork
	invoke ExitProcess,0
	ret
main endp

dowork proc
	invoke CLSIDFromProgID,addr lpProgID,addr rclsid
	invoke OleInitialize,0
	invoke CoCreateInstance,addr rclsid,NULL,CLSCTX_INPROC_SERVER + CLSCTX_LOCAL_SERVER,\
		addr IID_IDispatch,addr pIDispath
	.if eax == S_OK
		invoke GetUserDefaultLCID
		mov lcid,eax
		coinvoke pIDispath,IDispatch,GetIDsOfNames,addr IID_NULL,addr fn,1,lcid,addr dispid
		mov dsppar.rgvarg,NULL
		mov dsppar.rgdispidNamedArgs,NULL
		mov dsppar.cArgs,0
		mov dsppar.cNamedArgs,0
		coinvoke pIDispath,IDispatch,Invoke,dispid,addr IID_NULL,lcid,DISPATCH_METHOD,\
			addr dsppar,NULL,NULL,NULL
	.endif
	invoke OleUninitialize
	xor eax,eax
	ret
dowork endp
end start

      В ole32.dll находятся API-функции для работы с COM и OLE. В oaidl.inc структуры и константы выдранные из С++. А L.inc я подключаю для упрощения объявления unicode-строк в программе.В секции data две GUID-переменные. Одна нулевая - IID_NULL. Вторая - IID_IDispatch - это GUID IDispatch (данный GUID легко найти в реестре в HKEY-CLASSES-ROOT\Interface) его значение равно {00020400-0000-0000-C000-000000000046}. Кстати в этой ветке можно найти описания различных интерфейсов, присутствующих в вашей системе. Кроме этого мы создаем переменные размером в двойное слово для указателя Dispatch интерфейса - pIDispatch, для CLSID объекта - rclsid, для диспетчерского идентификатора DISPID (объясню позднее) и для локального системного идентификатора - lcid. Затем, используя макрос из L.inc, мы инициализируем unicode-строку ProgID нашего объекта. И, наконец, название вызываемого метода (функции) COM-объекта в виде массива слов и dword переменная-указатель на этот массив. В секции неинициализированных данных data? создаем переменную структуры DISPPARAMS - dsppar.

      Теперь по пунктам.

invoke CLSIDFromProgID,addr lpProgID,addr rclsid

      Вот описание функции из MSDN:

HRESULT CLSIDFromProgID(
LPCOLESTR lpszProgID,	//Указатель на ProgID объекта
LPCLSID   pclsid       //Указатель на CLSID объекта
);

      Функция принимает в качестве входного параметра ProgID COM-объекта и возвращает CLSID данного объекта. Если бы мы имели GUID объекта, то смогли бы использовать другую API-функцию -

HRESULT CLSIDFromString(
LPCOLESTR lpsz,	//Указатель на строковое представление CLSID объекта
LPCLSID   pclsid       //Указатель на CLSID объекта
);

которая позволяет получить CLSID объекта из указателя на строку GUID.

invoke OleInitialize,0
...
invoke OleUninitialize

      Эта пара функций используется для инициализации/деинициализации библиотеки COM. Функция OleInitialize имеет единственный (причем зарезервированный параметр), который должет быть равен нулю. Кстати практика показывает, что вместо этой пары функций вполне можно использовать другую пару:

invoke CoInitialize,0
...
invoke CoUninitialize

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

invoke CoCreateInstance,addr rclsid,NULL,CLSCTX_INPROC_SERVER + CLSCTX_LOCAL_SERVER,\
		addr IID_IDispatch,addr pIDispath

      Главная функция создания экземпляра COM-объекта. Ее описание в MSDN выглядит следующим образом:


STDAPI CoCreateInstance(
  REFCLSID    rclsid,       //идентификатор класса (CLSID) объекта
  LPUNKNOWN   pUnkOuter,    //указатель на IUnknown
  DWORD       dwClsContext, //флаг(и) контекста, в котором объект будет запущен на выполнение
  REFIID      riid,        //указатель на идентификатор интерфейса
  LPVOID *   ppv           //адрес переменной в которую будет возвращен указатель на 
				//запрашиваемый интерфейс riid
);

      Думаю здесь все ясно. Второй параметр у нас NULL, т.к. мы не нуждаемся в IUnknown. Третий параметр - это одна или несколько объединенных констант энумератора CLSCTX:


typedef enum tagCLSCTX 
{ 
    CLSCTX_INPROC_SERVER    = 1, 
    CLSCTX_INPROC_HANDLER   = 2, 
    CLSCTX_LOCAL_SERVER     = 4 
    CLSCTX_REMOTE_SERVER    = 16
    CLSCTX_NO_CODE_DOWNLOAD = 400
    CLSCTX_NO_FAILURE_LOG = 4000
} CLSCTX; 
#define CLSCTX_SERVER    (CLSCTX_INPROC_SERVER | CLSCTX_LOCAL_SERVER | CLSCTX_REMOTE_SERVER)
#define CLSCTX_ALL       (CLSCTX_INPROC_HANDLER | CLSCTX_SERVER)

Четвертый параметр - это GUID запрашиваемого интерфейса (в данном случае мы запрашиваем указатель на IDispatch). И, наконец пятый параметр - переменная, в которую этот указатель вернется в случае удачного выполнения функции.

      В случае успешного выполнения, функция возвращает S_OK (или попросту говоря 0). А в случае неудачи - одну из трех ошибок (REGDB_E_CLASSNOTREG (класс не зарегестрирован), CLASS_E_NOAGGREGATION (класс не может быть создан как часть вызывающего процесса), E_NOINTERFACE (интерфейс отсутствует)).

invoke GetUserDefaultLCID

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

coinvoke pIDispath,IDispatch,GetIDsOfNames,addr IID_NULL,addr fn,1,lcid,addr dispid

      А вот об этом поподробнее, пожалуй. coinvoke - это макрос, опеределенный в файле oaidl.inc. Кстати авторство этого файла и нескольких других, предназначенных для работы с COM-объектами, пренадлежит Ernest Murphy (Эрнест Мерфи). Макросу необходимо передать указатель на интерфейс, название интерфейса, название вызываемой функции (метода) и, что не является обязательным, список аргументов (параметров) функции (метода).

      А теперь внимание. Если Вы не поймете этот момент, то не поймете ничего вообще. Мы в данном случае вызываем функцию GetIDsOfNames интерфейса IDispatch. Сама функция GetIDsOfNames служит для получения указателя на интерфейс необходимого нам метода mymethod и требует пять параметров:

HRESULT GetIDsOfNames( 
  REFIID               riid,        // зарезервировано, должно быть IID_NULL
  OLECHAR FAR* FAR*   rgszNames,   // массив указателей на запрашиваемые методы объекта
  unsigned int        cNames,      // количество элементов массива
  LCID                lcid,        // локальный контекст
  DISPID FAR*        rgDispId       // массив указателей на запрашиваемые методы объекта,
                                // каждый элемент которого ID соответствует элементу массива rgszNames
                                // Это и есть пресловутый DISPID
  );

      Поэтому у нас в coinvoke восемь параметров: три, обязательных для макроса, плюс пять для функции. Надеюсь Вам это стало также понятно, как мне недавно. :-))

mov dsppar.rgvarg,NULL
mov dsppar.rgdispidNamedArgs,NULL
mov dsppar.cArgs,0
mov dsppar.cNamedArgs,0
coinvoke pIDispath,IDispatch,Invoke,dispid,addr IID_NULL,lcid,DISPATCH_METHOD,\
	addr dsppar,NULL,NULL,NULL

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


HRESULT Invoke( 
  DISPID            dispIdMember, // dispid метода
  REFIID            riid,         // зарезервировано,должно быть IID_NULL
  LCID              lcid,         // локальный контекст
  WORD              wFlags,       // флаг,описывающий контекст вызова Invoke
  DISPPARAMS FAR*  pDispParams,  // указатель на структуру DISPPARAMS со списком аргументов вызываемого метода
  VARIANT FAR*     pVarResult,   // указатель на возвращаемый методом результат или NULL,если результат не нужен
  EXCEPINFO FAR*   pExcepInfo,   // указатель на структуру,содержащую информацию об ошибках, или NULL
  unsigned int FAR* puArgErr    // индекс аргумента структуры DISPPARAMS,вызвавшего ошибку
  );

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


DISPATCH_METHOD
DISPATCH_PROPERTGET
DISPATCH_PROPERTYPUT
DISPATCH_PROPERTYPUTREF

      Пятый параметр - структура DISPPARAMS, содержащая аргументы вызываемой функции COM-объекта. Вот ее определение:


typedef struct tagDISPPARAMS {
VARIANTARG*      rgvarg;             // Массив аргументов
DISPID*          rgdispidNamedArgs;  // DISPID для именованных аргументов
unsigned int    cArgs;              // Число аргументов
unsigned int    cNamedArgs;        // Число именованных аргументов
} DISPPARAMS;

      Иначе говоря, если нам для функции Color объекта необходимо передать аргумент Red, то этот аргумент мы передаем в пятом параметре функции Invoke. Так как в настоящий момент метод mymethod нашего COM-объекта не требует никаких параметров, то мы в первые два элемента структуры передаем NULL (у нас нет аргументов и именованных аргументов), а во вторые два элемента передаем 0 (количество аргументов и именованных аргументов). Но запомним эту структуру на будущее, т.к. она нам будет очень нужна при передаче аргументов функций в будущем.

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

      Думаю сейчас Вы уже наслаждаетесь видом окна сообщения, выводимого нашим COM-объектом. Но все это еще только цветочки. Дальше нас ждут Outproc-сервера и удаленный доступ к COM-объектам. А сейчас можете пойти и попить пивка. :-))


© 2005 ironahot@idknet.com - при использовании статей просьба делать ссылку на автора