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


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

Статья вторая

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

      По сути Outproc-сервер ничем не отличается от Inproc-сервера кроме расширения файла. Поскольку он должен выполняться в отдельном от вызывающей программы адресном пространстве, то компилируется, как файл с расширением *.exe. В результате получается отдельный исполняемый файл со своим циклом сообщений и собственным потоком. А это значит, что вызовы от клиента к серверу должны каким-то образом передаваться через границы потоков. Эту функциональность призван обеспечить маршалинг вызовов.

Маршалинг
рис.1 Понятие маршалинга

      Честно говоря, я не хочу сейчас заострять Ваше внимание на маршалинге, потому что в большинстве случаев COM-объект сам заботится о выполнении маршалинга и синхронизации вызовов. Вместо этого приступим к написанию простого Outproc-сервера. Для этого опять воспользуемся Visual Basic. Выполняйте все шаги по созданию проекта так же, как это описано в моей прошлой статье, но только в качестве проекта выберите ActiveX Exe. Дайте имена проекту, классу и методу на свой вкус и скомпилируйте проект в файл с расширением *.exe. После этого возьмите пример клиента из прошлой статьи, замените в нем lpProgID и FuncName на выбранные Вами для Вашего сервера. После компиляции Ваш клиент готов к работе с внешним COM-сервером.

      Довольно интересно запускать клиента с помощью дебагера, например OllyDbg. После выполнения функции CoCreateInstance, Вы можете запустить Task Manager и полюбоваться на Ваш объект, висящий в списке запущенных процессов.

      Теперь, когда Вы закончили медитировать, продолжим. Итак Вы видите, что при использовании интерфейса IDispatch, для нас нет никакой разницы с каким сервером мы работаем: внутренним или внешним. В этом сила COM.

      Внешние удаленные сервера

      Но все меняется, когда приходят они
       (рекламное)

      Представьте себе, что Вам позарез понадобилось получить доступ из программы-клиента к COM-серверу, расположенному на другом компьютере Вашей локальной сети. Просто вызвать объект так, как мы это делали ранее у Вас не получится, по той простой причине, что клиент знать не знает ни о каком сервере, до тех пор, пока тот не будет зарегистрирован в реестре. Ведь именно из реестра функция CoCreateInstance черпает информацию об объекте. А если мы зарегистрируем компонент в локальном реестре, он перестанет быть удаленным. Вот тут-то и приходит на помощь расширенная функция CoCreateInstanceEx, принимающая в качестве параметров не только GUID IDispatch, но и название компьютера, на котором COM-сервер зарегистрирован. Но... давайте по порядку.

      Для начала напишем COM-сервер с помощью VB. Как и для первого примера этой статьи, выбираем проект ActiveX Exe, меняем название проекта на remote_com, название класса на remote_class, а методу дадим имя remote_method.

remote_com
рис.2 Создание COM-сервера для удаленного использования

      В методе класса я определил один строковый параметр strIn, получающий значение по значению. Кроме того, сам метод возвращает вызывающему клиенту строку. Этим я собираюсь убить трех зайцев: показать как работать с COM-объектом на удаленной машине, передавать объекту параметры и получать назад результат выполнения метода (функции) COM-объекта. В меню Project выберите пункт remote_com Properties.... Появится диалоговое окно:

remote_com properties
рис.2 Свойства проекта

Перейдите на вкладку Component и в разделе Version Compatibility нажмите радио-кнопку Binary Compatibility. Если Вы желаете в дальнейшем распространять вместе с COM-объектом библиотеку типов (файл с расширением *.tlb), то в разделе Remote Server отметьте чекбокс Remote Server Files. Тогда Visual Basic сгенерирует все необходимые файлы для регистрации COM-объекта на машине клиента. Нам сейчас это, в принципе, не нужно. Так как мы может сделать обычный COM-объект, а в DCOM он превратится, если будет расположен на другой машине. Но знать об этом не лишне. Вообще-то COM от DCOM отличается тем, что первый мы вызываем посредством использования COM библиотеки локальной машины, а при обращении к удаленному COM-объекту, мы задействуем сервер Автоматизации, который посредством RPC (Remote Procedure Call), осуществляет вызовы по сети... Очень упрощенно. :-))

      Не забудьте, что при компиляции, Visual Basic автоматически регистрирует сервер. Поэтому, если вы собираетесь проверить его работоспособность на другой машине, то после компиляции разрегестрируйте компонент. Для этого я обычно пользуюсь программой Com Explorer. Затем перенесите файл remote_com.exe на другую машину (лучше в системную папку, хотя не обязательно) и запустите на выполнение. COM-объект сам зарегистрирует себя в реестре. Если Вы не являетесь администратором сети, то для получения доступа к удаленному COM-объекту, на компьютере, где Вы установили его, запустите утилиту dcomcnfg.exe, входящую в поставку Windows. В ветке DCOM Config найдите remote_com.remote_class и, кликнув по его названию правой кнопкой мыши, выберите пункт Properties. В появившемся диалоговом окне перейдите на вкладку Security, на которой в разделе Access Permissions выберите Customize и с помощью кнопки Edit вызовите диалоговое окно настройки доступа. Добавьте в качестве пользователей COM-объекта Domain Admins и Domain Users. Это позволит обращаться к объекту администраторам и пользователям домена Вашей сети.

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

.586
.model flat, stdcall
option casemap :none

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

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

	COSERVERINFO struct
		dwReserved1 DWORD ?
		pwszName DWORD ?
		pAuthInfo DWORD ?
		dwReserved2 DWORD ?
	COSERVERINFO ends

	MULTI_QI struct
		piid DWORD ?
		pItf HANDLE ?
		hr DWORD ?
	MULTI_QI ends

	main proto
	dowork proto
	
.data
	AppName wchar L(<Remote COM-object Call\0>)
	dispid dd 0
	rclsid dd 0
	dclsid dd 0
	pIDispath dd 0
	lcid dd 0
	fn dd offset FuncName
	FuncName dw 'r','e','m','o','t','e','_','m','e','t','h','o','d',0,0,0
	lpClassGuid wchar L(<{cb211c11-84b7-4b57-896b-4e3f3a5d855a}\0>) // подставьте GUID объекта
	lpSrvName wchar L(<ip-локального-или-удаленного-компьютера\0>)
	IID_NULL GUID <0,0,0,<0,0,0,0,0,0,0,0>>
	IID_IDispatch db "{00020400-0000-0000-C000-000000000046}",0
	param_in wchar L(<some string\0>)

.data?
	wDispatch db 100 dup(?)
	dsppar DISPPARAMS <?>
	ServerInfo COSERVERINFO <?>
	mqi MULTI_QI <?>

.code
start:
main proc
	invoke dowork
	invoke ExitProcess,0
	ret
main endp

dowork proc
LOCAL bstr_in:BSTR
LOCAL bstr_out:BSTR
LOCAL varg_in:VARIANTARG
LOCAL varg_out:VARIANTARG
	invoke GlobalAlloc,GMEM_FIXED,16
	mov mqi.piid,eax
	invoke MultiByteToWideChar,CP_ACP,0,addr IID_IDispatch,sizeof IID_IDispatch,\
		addr wDispatch,sizeof wDispatch
	invoke IIDFromString,addr wDispatch,mqi.piid
	invoke CLSIDFromString,addr lpClassGuid,addr rclsid
	lea eax,offset lpSrvName
	mov ServerInfo.pwszName,eax
	invoke OleInitialize,0
	invoke CoCreateInstanceEx,addr rclsid,0,CLSCTX_REMOTE_SERVER,addr ServerInfo,1,addr mqi
	.if eax == S_OK
		invoke GetUserDefaultLCID
		mov lcid,eax
		coinvoke mqi.pItf,IDispatch,GetIDsOfNames,addr IID_NULL,addr fn,1,lcid,addr dispid
		invoke VariantInit,addr varg_in
		mov varg_in.vt, VT_BSTR
		invoke SysAllocString,addr param_in
		mov bstr_in,eax
		mov varg_in.bstrVal,eax
		lea eax,varg_in
		mov dsppar.rgvarg,eax
		mov dsppar.rgdispidNamedArgs,NULL
		mov dsppar.cArgs,1
		mov dsppar.cNamedArgs,0
		invoke VariantInit,addr varg_out
		mov varg_out.vt,VT_BSTR
		mov eax,bstr_out
		mov varg_out.bstrVal,eax
		coinvoke mqi.pItf,IDispatch,Invoke,dispid,addr IID_NULL,lcid,DISPATCH_METHOD,\
			addr dsppar,addr varg_out,NULL,NULL
		invoke SysFreeString,bstr_out
		invoke SysFreeString,bstr_in
		invoke MessageBoxW,0,varg_out.bstrVal,addr AppName,0
	.endif
	invoke OleUninitialize
	invoke GlobalFree,mqi.piid
	xor eax,eax
	ret
dowork endp
end start

      Как вы заметили в секции data произошли некоторые изменения. Во первых имя приложения я инициализировал, как unicode-строку. Зачем? Узнаете позже. Вместо имени COM-объекта, я использую его GUID. Это потому, что я допускаю, что библиотека типов COM-объекта не зарегистрирована на компьютере клиента и, поэтому в реестре нет никаких записей о нем. Следовательно мы вынуждены передать серверу Автоматизации уникальный идентификатор объекта, чтобы он смог сам найти, на каком компьютере сети задействовать объект. Далее идет IP-сервера (можно использовать имя компьютера). Если вы инициализируете клиента и COM-сервер на одном компьютере, то укажите IP локального компьютера в виде 127.0.0.1. И в конце секции присутствует unicode-переменная param_in. Ее я использую для передачи строкового параметра методу COM-объекта.

      В секции data? я инициализирую структуры DISPPARAMS (для передаче параметров методу COM-объекта), COSERVERINFO и MULTI_QI. Структура MULTI_QI в MSDN выглядит так:


typedef struct _MULTI_QI {
        const IID*    pIID;  // Указатель на идентификатор интерфейса
        IUnknown *    pItf;  // Указатель на интерфейс,запрашиваемый в pIID.Должен быть 
				// установлен в NULL
        HRESULT       hr;   // Значение,возвращаемое вызовом QueryInterface,необходимое 
			// для запроса интерфейса, определенного в pIID.Обычно возвращает S_OK или 
			// E_NOINTERFACE. Перед использованием должно быть установлено в 0.
    } MULTI_QI;

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

invoke GlobalAlloc,GMEM_FIXED,16
mov mqi.piid,eax

      Этими строками кода мы резервируем память под структуру MULTI_QI, и затем указатель на блок памяти передаем первому параметру структуры MULTI_QI.

invoke MultiByteToWideChar,CP_ACP,0,addr IID_IDispatch,sizeof IID_IDispatch,\
	addr wDispatch,sizeof wDispatch
invoke IIDFromString,addr wDispatch,mqi.piid

      Переводим байтовую строку GUID IDispatch в unicode. Затем вызываем функцию IIDFromString для конвертации этой строки в идентификатор интерфейса:


WINOLEAPI IIDFromString(
LPOLESTR lpsz,  //Указатель на строковое представление IID
LPIID    lpiid  //Указатель на запрашиваемый интерфейс
);

      Далее мы вызываем функцию CLSIDFromString. Как я уже рассказывал ранее, она предназначена для получения CLSID СOM-объекта из строкового представления в unicode.

lea eax,offset lpSrvName
mov ServerInfo.pwszName,eax

      Структура COSERVERINFO в MSDN выглядит так:


typedef struct  _COSERVERINFO
    {
    DWORD dwReserved1;	// зарезервировано, должно быть 0
    LPWSTR pwszName;        // Указатель на имя компьютера
    COAUTHINFO  *pAuthInfo; // Указатель на структуру COAUTHINFO.Использовать не будем.
    DWORD dwReserved2;      // зарезервировано, должно быть 0
    }   COSERVERINFO;

      Таким образом, нас интересует только второй элемент структуры, в который мы должны передать имя компьютера, на котором зарегестрирован COM-сервер. Заметьте, что имя компьютера у нас также в виде unicode-строки.

invoke OleInitialize,0
...
invoke OleUninitialize

      Стандартная инициализация и деинициализация библиотеки COM.

invoke CoCreateInstanceEx,addr rclsid,0,CLSCTX_REMOTE_SERVER,addr ServerInfo,1,addr mqi

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


HRESULT CoCreateInstanceEx(
REFCLSID         rclsid,    //CLSID создаваемого объекта
IUnknown *       punkOuter,  //IUnknown (не используем)
DWORD            dwClsCtx,   //флаги энумератора CLSCTX
COSERVERINFO *  pServerInfo, //Компьютер,на котором установлен объект
ULONG            cmq,        //Количество структур MULTI_QI,указанных в pResults
MULTI_QI*       pResults     //Массив структур MULTI_QI
);

      Надеюсь до сих пор все понятно. Мы подготовили структуры MULTI_QI и COSERVERINFO, а затем передали их в качестве параметров функции CoCreateInstanceEx. Эта функция за нас осуществляет обращение к внутренней функции QueryInterface и возвращает нам назад заполненную указателями на интерфейсы объекта структуру MULTI_QI.

invoke GetUserDefaultLCID
mov lcid,eax
coinvoke mqi.pItf,IDispatch,GetIDsOfNames,addr IID_NULL,addr fn,1,lcid,addr dispid

      Даже не хочу останавливаться на этом моменте. Все объяснено в первой статье.

invoke VariantInit,addr varg_in
mov varg_in.vt, VT_BSTR
invoke SysAllocString,addr param_in
mov bstr_in,eax
mov varg_in.bstrVal,eax

      Мы не можем просто передать аргументы вызываемому методу COM-объекта в виде строки. Перед этим необходимо немного поработать головой. Структура DISPPARAMS принимает аргументы не в виде строки, а в виде массива другой структуры, которая называется VARIANTARG (она же VARIANT). В файле oaidl.inc она опеределена так:


VARIANT STRUCT
    vt              WORD            VT_EMPTY
    wReserved1      WORD            0
    wReserved2      WORD            0
    wReserved3      WORD            0
    Union
        lVal            SDWORD          ?   ; VT_I4
        bVal            WORD            ?   ; VT_UI1
        iVal            SWORD           ?   ; VT_I2
        fltVal          REAL4           ?   ; VT_R4
        dblVal          REAL8           ?   ; VT_R8
        boolVal         VARIANT_BOOL    ?   ; VT_BOOL
        scode           SCODE           ?   ; VT_ERROR
        cyVal           QWORD           ?   ; VT_CY
        date            QWORD           ?   ; VT_DATE
        bstrVal         BSTR            ?   ; VT_BSTR
        punkVal         PVOID           ?   ; VT_UNKNOWN
        pdispVal        PVOID           ?   ; VT_DISPATCH
        parray          PVOID           ?   ; VT_ARRAY
        pbVal           PVOID           ?   ; VT_BYREF|VT_UI1
        piVal           PVOID           ?   ; VT_BYREF|VT_I2
        plVal           PVOID           ?   ; VT_BYREF|VT_I4
        pfltVal         PVOID           ?   ; VT_BYREF|VT_R4
        pdblVal         PVOID           ?   ; VT_BYREF|VT_R8
        pboolVal        PVOID           ?   ; VT_BYREF|VT_BOOL
        pscode          PVOID           ?   ; VT_BYREF|VT_ERROR
        pcyVal          PVOID           ?   ; VT_BYREF|VT_CY
        pdate           PVOID           ?   ; VT_BYREF|VT_DATE
        pbstrVal        PVOID           ?   ; VT_BYREF|VT_BSTR
        ppunkVal        PVOID           ?   ; VT_BYREF|VT_UNKNOWN
        ppdispVal       PVOID           ?   ; VT_BYREF|VT_DISPATCH
        pparray         PVOID           ?   ; VT_BYREF|VT_ARRAY
        pvarVal         PVOID           ?   ; VT_BYREF|VT_VARIANT
        byref           PVOID           ?   ; Generic ByRef
        cVal            SBYTE           ?   ; VT_I1
        uiVal           WORD            ?   ; VT_UI2
        ulVal           DWORD           ?   ; VT_UI4
        intVal          SWORD           ?   ; VT_int
        uintVal         WORD            ?   ; VT_uint
        pdecVal         PVOID           ?   ; VT_BYREF|VT_DECIMAL
        pcVal           PVOID           ?   ; VT_BYREF|VT_I1
        puiVal          PVOID           ?   ; VT_BYREF|VT_UI2
        pulVal          PVOID           ?   ; VT_BYREF|VT_UI4
        pintVal         PVOID           ?   ; VT_BYREF|VT_int
        puintVal        PVOID           ?   ; VT_BYREF|VT_uint
    ENDS
VARIANT ENDS

      Как Вы видите, VARIANT это объединение различных типов данных, с которыми может оперировать COM-объект. Для нас же важным является то, что disp-интерфейсы и дуальные интерфейсы могут передавать только те типы данных, которые можно выразить при помощи типов, определенных в структуре VARIANT. Для инициализации структуры VARIANT предназначена функция VariantInit. Она передает в первый элемент структуры VT_EMPRY. Реально в структуре VARIANT используются только первый и пятый элементы, т.к. остальные три зарезервированы для использования в будущем. В первый элемент vt структуры мы передаем флаг типа данных, которые мы собираемся передавать в качестве аргумента нашему методу. Поскольку Visual Basic воспринимает строки в виде т.н. BSTR-типа, то мы передаем флаг VT_BSTR. Затем unicode-строку аргумента нам необходимо привести к данному типу данных, для чего мы используем функцию SysAllocString, используемую в паре с функцией SysFreeString.


BSTR SysAllocString( 
  const OLECHAR *  sz);

      Эта функция резервирует память под строку BSTR и затем копирует в этот блок памяти unicode-строку, передаваемую ей в качестве параметра. Возвращаемым значением является указатель на блок памяти с BSTR-строкой. Кстати, передать этой функции можно и пустую строку, что мы и делаем для возвращаемого методом параметра. После получения BSTR-строки, мы передаем ее второму элементу структуры VARIANT, как значение bstrVal.

lea eax,varg_in
mov dsppar.rgvarg,eax
mov dsppar.rgdispidNamedArgs,NULL
mov dsppar.cArgs,1
mov dsppar.cNamedArgs,0

      После этого, мы спокойно можем инициализировать структуру DISPPARAMS, передав в rgvarg адрес структуры VARIANTARG, и указав в cArgs, что у нас имеется один передаваемый методу COM-объекта аргумент. Так как мы не используем именованные аргументы, то в элементы rgdispidNamedArgs и cNamedArgs мы передаем NULL и 0 соответственно.

invoke VariantInit,addr varg_out
mov varg_out.vt,VT_BSTR
mov eax,bstr_out
mov varg_out.bstrVal,eax

      Знакомая Вам уже инициализация структуры VARIANTARG, но только для возвращаемого методом COM-объекта значения. Как Вы помните метод нам должен вернуть строку (соответственно BSTR).

coinvoke mqi.pItf,IDispatch,Invoke,dispid,addr IID_NULL,lcid,DISPATCH_METHOD,\
	addr dsppar,addr varg_out,NULL,NULL
invoke SysFreeString,bstr_out
invoke SysFreeString,bstr_in
invoke MessageBoxW,0,varg_out.bstrVal,addr AppName,0

      Вызываем на выполнение метод, передавая функции Invoke интерфейса шестым параметром адрес выходной структуры VARIANTARG. После отработки Invoke, идет стандартная очистка обоих блоков памяти, зарезервированных ранее под BSTR-строки. После этого вызываем unicode-функцию MessageBoxW для отображения возвращаемой методом строки.

Всё...

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


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